fix
This commit is contained in:
@@ -99,8 +99,22 @@ const confirmationIdFromPreview = (value: unknown) => {
|
|||||||
return typeof confirmationId === 'string' && confirmationId ? confirmationId : undefined
|
return typeof confirmationId === 'string' && confirmationId ? confirmationId : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountTypeSchema = z.enum(['general', 'specific', 'nisa', 'juniorNisa', 'unknown'])
|
const accountTypeSchema = z.enum([
|
||||||
const depositTypeSchema = z.enum(['general', 'specific', 'nisa', 'juniorNisa', 'unknown'])
|
'general',
|
||||||
|
'specific',
|
||||||
|
'growthInvestment',
|
||||||
|
'nisa',
|
||||||
|
'juniorNisa',
|
||||||
|
'unknown',
|
||||||
|
])
|
||||||
|
const depositTypeSchema = z.enum([
|
||||||
|
'general',
|
||||||
|
'specific',
|
||||||
|
'growthInvestment',
|
||||||
|
'nisa',
|
||||||
|
'juniorNisa',
|
||||||
|
'unknown',
|
||||||
|
])
|
||||||
const tradeSideSchema = z.enum(['buy', 'sell'])
|
const tradeSideSchema = z.enum(['buy', 'sell'])
|
||||||
const marketCodeSchema = z.string().min(1).describe('SBI market code')
|
const marketCodeSchema = z.string().min(1).describe('SBI market code')
|
||||||
const issueCodeSchema = z.string().min(1).describe('Issue code')
|
const issueCodeSchema = z.string().min(1).describe('Issue code')
|
||||||
@@ -202,6 +216,9 @@ const cashOrderPriceConditionSchema = z.enum([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const cashOrderSchema = stockOrderBaseSchema.extend({
|
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'),
|
price: z.number().positive().optional().describe('Order price for price-based orders'),
|
||||||
kind: z.enum(['market', 'limit', 'stop', 'oco', 'ifd', 'ifdo', 's', 'unknown']).optional(),
|
kind: z.enum(['market', 'limit', 'stop', 'oco', 'ifd', 'ifdo', 's', 'unknown']).optional(),
|
||||||
priceCondition: cashOrderPriceConditionSchema
|
priceCondition: cashOrderPriceConditionSchema
|
||||||
@@ -219,21 +236,92 @@ const cashOrderSchema = stockOrderBaseSchema.extend({
|
|||||||
.optional()
|
.optional()
|
||||||
.describe('Secondary execution condition for OCO orders'),
|
.describe('Secondary execution condition for OCO orders'),
|
||||||
secondaryPrice: z.number().positive().optional().describe('Secondary price 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
|
sorLastMarket: marketCodeSchema
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Previous market code sent with SOR orders; defaults to login profile'),
|
.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({
|
const placeCashOrderSchema = cashOrderSchema.extend({
|
||||||
confirmationId: z
|
confirmationId: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Confirmation ID returned by the confirmation step'),
|
.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'),
|
allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live order'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const orderCorrectionSchema = z.object({
|
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,
|
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'),
|
quantity: z.number().positive().optional().describe('Corrected order quantity'),
|
||||||
price: z.number().positive().optional().describe('Corrected order price'),
|
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'),
|
.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({
|
const placeMarginOpenOrderSchema = marginOpenOrderSchema.extend({
|
||||||
confirmationId: z
|
confirmationId: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Confirmation ID returned by the confirmation step'),
|
.describe('Confirmation ID returned by the confirmation step'),
|
||||||
|
omitConfirmation: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe('APK confirmation-screen omission flag for live submit calls'),
|
||||||
allowTrading: z
|
allowTrading: z
|
||||||
.literal(true)
|
.literal(true)
|
||||||
.optional()
|
.optional()
|
||||||
@@ -275,9 +390,31 @@ const placeMarginOpenOrderSchema = marginOpenOrderSchema.extend({
|
|||||||
|
|
||||||
const marginCloseOrderSchema = cashOrderSchema.extend({
|
const marginCloseOrderSchema = cashOrderSchema.extend({
|
||||||
positionId: positionIdSchema.optional().describe('Position ID to close'),
|
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({
|
const placeMarginCloseOrderSchema = marginCloseOrderSchema.extend({
|
||||||
|
omitConfirmation: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe('APK confirmation-screen omission flag for live submit calls'),
|
||||||
allowTrading: z
|
allowTrading: z
|
||||||
.literal(true)
|
.literal(true)
|
||||||
.optional()
|
.optional()
|
||||||
@@ -293,6 +430,22 @@ const actualDeliveryOrderSchema = z.object({
|
|||||||
price: z.number().positive().optional().describe('Order price for price-based requests'),
|
price: z.number().positive().optional().describe('Order price for price-based requests'),
|
||||||
kind: z.enum(['genbiki', 'genwatashi']),
|
kind: z.enum(['genbiki', 'genwatashi']),
|
||||||
positionId: positionIdSchema.optional().describe('Position ID to deliver'),
|
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({
|
const placeActualDeliveryOrderSchema = actualDeliveryOrderSchema.extend({
|
||||||
@@ -300,6 +453,10 @@ const placeActualDeliveryOrderSchema = actualDeliveryOrderSchema.extend({
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Confirmation ID returned by the confirmation step'),
|
.describe('Confirmation ID returned by the confirmation step'),
|
||||||
|
omitConfirmation: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe('APK confirmation-screen omission flag for live submit calls'),
|
||||||
allowTrading: z
|
allowTrading: z
|
||||||
.literal(true)
|
.literal(true)
|
||||||
.optional()
|
.optional()
|
||||||
@@ -311,6 +468,39 @@ const ifdOrderSchema = cashOrderSchema.extend({
|
|||||||
.enum(['cash', 'marginOpen'])
|
.enum(['cash', 'marginOpen'])
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Product to use for the first IFD leg'),
|
.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({
|
const placeIfdOrderSchema = ifdOrderSchema.extend({
|
||||||
@@ -318,15 +508,58 @@ const placeIfdOrderSchema = ifdOrderSchema.extend({
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Confirmation ID returned by the confirmation step'),
|
.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'),
|
allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live IFD order'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const themeInvestmentOrderSchema = z.object({
|
const themeInvestmentOrderSchema = z.object({
|
||||||
themeId: z.string().min(1).describe('Theme ID for the theme investment order'),
|
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,
|
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'),
|
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({
|
const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({
|
||||||
allowTrading: z
|
allowTrading: z
|
||||||
.literal(true)
|
.literal(true)
|
||||||
@@ -366,6 +599,7 @@ const methodParamSchemas = {
|
|||||||
'watchlist.list': undefined,
|
'watchlist.list': undefined,
|
||||||
'orders.inquiry.executionsToday': orderInquiryOptionsSchema.optional(),
|
'orders.inquiry.executionsToday': orderInquiryOptionsSchema.optional(),
|
||||||
'orders.inquiry.open': orderInquiryOptionsSchema.optional(),
|
'orders.inquiry.open': orderInquiryOptionsSchema.optional(),
|
||||||
|
'orders.cash.preOrder': cashOrderPreOrderSchema,
|
||||||
'orders.cash.estimate': cashOrderSchema,
|
'orders.cash.estimate': cashOrderSchema,
|
||||||
'orders.cash.place': placeCashOrderSchema,
|
'orders.cash.place': placeCashOrderSchema,
|
||||||
'orders.cash.estimateCorrection': orderCorrectionSchema,
|
'orders.cash.estimateCorrection': orderCorrectionSchema,
|
||||||
@@ -373,23 +607,26 @@ const methodParamSchemas = {
|
|||||||
'orders.cash.placeCorrection': placeOrderCorrectionSchema,
|
'orders.cash.placeCorrection': placeOrderCorrectionSchema,
|
||||||
'orders.cash.estimateCancel': orderCancelSchema,
|
'orders.cash.estimateCancel': orderCancelSchema,
|
||||||
'orders.cash.placeCancel': placeOrderCancelSchema,
|
'orders.cash.placeCancel': placeOrderCancelSchema,
|
||||||
|
'orders.margin.preOrderOpen': marginOpenOrderPreOrderSchema,
|
||||||
'orders.margin.estimateOpen': marginOpenOrderSchema,
|
'orders.margin.estimateOpen': marginOpenOrderSchema,
|
||||||
'orders.margin.open': placeMarginOpenOrderSchema,
|
'orders.margin.open': placeMarginOpenOrderSchema,
|
||||||
|
'orders.margin.preOrderClose': marginCloseOrderPreOrderSchema,
|
||||||
'orders.margin.estimateClose': marginCloseOrderSchema,
|
'orders.margin.estimateClose': marginCloseOrderSchema,
|
||||||
'orders.margin.close': placeMarginCloseOrderSchema,
|
'orders.margin.close': placeMarginCloseOrderSchema,
|
||||||
'orders.margin.estimateCloseSummary': marginCloseOrderSchema,
|
'orders.margin.estimateCloseSummary': marginCloseOrderSchema,
|
||||||
'orders.margin.closeSummary': placeMarginCloseOrderSchema,
|
'orders.margin.closeSummary': placeMarginCloseOrderSchema,
|
||||||
'orders.margin.estimateSummary': marginCloseOrderSchema,
|
'orders.margin.estimateSummary': marginCloseOrderSchema,
|
||||||
'orders.margin.placeSummary': placeMarginCloseOrderSchema,
|
'orders.margin.placeSummary': placeMarginCloseOrderSchema,
|
||||||
|
'orders.margin.preOrderActualDelivery': actualDeliveryOrderPreOrderSchema,
|
||||||
'orders.margin.estimateActualDelivery': actualDeliveryOrderSchema,
|
'orders.margin.estimateActualDelivery': actualDeliveryOrderSchema,
|
||||||
'orders.margin.actualDelivery': placeActualDeliveryOrderSchema,
|
'orders.margin.actualDelivery': placeActualDeliveryOrderSchema,
|
||||||
'orders.ifd.estimate': ifdOrderSchema,
|
'orders.ifd.estimate': ifdOrderSchema,
|
||||||
'orders.ifd.place': placeIfdOrderSchema,
|
'orders.ifd.place': placeIfdOrderSchema,
|
||||||
'orders.ifd.estimateCorrection': orderCorrectionSchema,
|
'orders.ifd.estimateCorrection': orderCorrectionSchema,
|
||||||
'orders.ifd.placeCorrection': placeOrderCorrectionSchema,
|
'orders.ifd.placeCorrection': placeOrderCorrectionSchema,
|
||||||
'orders.ifd.estimateCancel': orderCorrectionSchema,
|
'orders.ifd.estimateCancel': orderCancelSchema,
|
||||||
'orders.ifd.placeCancel': placeOrderCorrectionSchema,
|
'orders.ifd.placeCancel': placeOrderCancelSchema,
|
||||||
'orders.themeInvestment.list': undefined,
|
'orders.themeInvestment.list': themeInvestmentPreOrderSchema,
|
||||||
'orders.themeInvestment.estimate': themeInvestmentOrderSchema,
|
'orders.themeInvestment.estimate': themeInvestmentOrderSchema,
|
||||||
'orders.themeInvestment.place': placeThemeInvestmentOrderSchema,
|
'orders.themeInvestment.place': placeThemeInvestmentOrderSchema,
|
||||||
} satisfies Record<RpcMethod, z.ZodType | undefined>
|
} satisfies Record<RpcMethod, z.ZodType | undefined>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const RPC_METHODS = [
|
|||||||
'watchlist.list',
|
'watchlist.list',
|
||||||
'orders.inquiry.executionsToday',
|
'orders.inquiry.executionsToday',
|
||||||
'orders.inquiry.open',
|
'orders.inquiry.open',
|
||||||
|
'orders.cash.preOrder',
|
||||||
'orders.cash.estimate',
|
'orders.cash.estimate',
|
||||||
'orders.cash.place',
|
'orders.cash.place',
|
||||||
'orders.cash.estimateCorrection',
|
'orders.cash.estimateCorrection',
|
||||||
@@ -39,14 +40,17 @@ export const RPC_METHODS = [
|
|||||||
'orders.cash.placeCorrection',
|
'orders.cash.placeCorrection',
|
||||||
'orders.cash.estimateCancel',
|
'orders.cash.estimateCancel',
|
||||||
'orders.cash.placeCancel',
|
'orders.cash.placeCancel',
|
||||||
|
'orders.margin.preOrderOpen',
|
||||||
'orders.margin.estimateOpen',
|
'orders.margin.estimateOpen',
|
||||||
'orders.margin.open',
|
'orders.margin.open',
|
||||||
|
'orders.margin.preOrderClose',
|
||||||
'orders.margin.estimateClose',
|
'orders.margin.estimateClose',
|
||||||
'orders.margin.close',
|
'orders.margin.close',
|
||||||
'orders.margin.estimateCloseSummary',
|
'orders.margin.estimateCloseSummary',
|
||||||
'orders.margin.closeSummary',
|
'orders.margin.closeSummary',
|
||||||
'orders.margin.estimateSummary',
|
'orders.margin.estimateSummary',
|
||||||
'orders.margin.placeSummary',
|
'orders.margin.placeSummary',
|
||||||
|
'orders.margin.preOrderActualDelivery',
|
||||||
'orders.margin.estimateActualDelivery',
|
'orders.margin.estimateActualDelivery',
|
||||||
'orders.margin.actualDelivery',
|
'orders.margin.actualDelivery',
|
||||||
'orders.ifd.estimate',
|
'orders.ifd.estimate',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.2",
|
"@simplewebauthn/browser": "^13.1.2",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"lucide-vue-next": "^1.0.0",
|
"lucide-vue-next": "^1.0.0",
|
||||||
"motion-v": "^2.3.0",
|
"motion-v": "^2.3.0",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vue-tsc": "^3"
|
"vue-tsc": "^3"
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ const activeTab = computed<RouteName>(() => {
|
|||||||
return routeNames.includes(route.name as RouteName) ? (route.name as RouteName) : 'portfolio'
|
return routeNames.includes(route.name as RouteName) ? (route.name as RouteName) : 'portfolio'
|
||||||
})
|
})
|
||||||
const showAuthGate = computed(() => true)
|
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 {
|
const {
|
||||||
status,
|
status,
|
||||||
@@ -67,6 +70,7 @@ const {
|
|||||||
quantityInput,
|
quantityInput,
|
||||||
priceInput,
|
priceInput,
|
||||||
chartMode,
|
chartMode,
|
||||||
|
chartRange,
|
||||||
showSearch,
|
showSearch,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
countryFilter,
|
countryFilter,
|
||||||
@@ -86,6 +90,7 @@ const {
|
|||||||
orderHistoryNotice,
|
orderHistoryNotice,
|
||||||
positions,
|
positions,
|
||||||
chartPricePoints,
|
chartPricePoints,
|
||||||
|
chartNotice,
|
||||||
pricePolling,
|
pricePolling,
|
||||||
selectedStock,
|
selectedStock,
|
||||||
orderQuantity,
|
orderQuantity,
|
||||||
@@ -93,6 +98,11 @@ const {
|
|||||||
cashOrderPrimaryRequiresPrice,
|
cashOrderPrimaryRequiresPrice,
|
||||||
cashOrderTriggerPrice,
|
cashOrderTriggerPrice,
|
||||||
cashOrderSecondaryPrice,
|
cashOrderSecondaryPrice,
|
||||||
|
cashOrderAccountTypeOptions,
|
||||||
|
cashOrderMarketOptions,
|
||||||
|
cashOrderTermOptions,
|
||||||
|
cashOrderDateOptions,
|
||||||
|
cashOrderPriceStep,
|
||||||
estimatedAmount,
|
estimatedAmount,
|
||||||
showPortfolioSpinner,
|
showPortfolioSpinner,
|
||||||
canRequestCashEstimate,
|
canRequestCashEstimate,
|
||||||
@@ -106,7 +116,6 @@ const {
|
|||||||
totalAssetValue,
|
totalAssetValue,
|
||||||
stockAssetRatio,
|
stockAssetRatio,
|
||||||
cashAssetRatio,
|
cashAssetRatio,
|
||||||
boxPlotStyle,
|
|
||||||
hasQuote,
|
hasQuote,
|
||||||
selectStock,
|
selectStock,
|
||||||
connect,
|
connect,
|
||||||
@@ -211,17 +220,23 @@ onMounted(async () => {
|
|||||||
v-model:quantity-input="quantityInput"
|
v-model:quantity-input="quantityInput"
|
||||||
v-model:price-input="priceInput"
|
v-model:price-input="priceInput"
|
||||||
v-model:chart-mode="chartMode"
|
v-model:chart-mode="chartMode"
|
||||||
|
v-model:chart-range="chartRange"
|
||||||
:viewed-stocks="viewedStocks"
|
:viewed-stocks="viewedStocks"
|
||||||
:selected-stock="selectedStock"
|
:selected-stock="selectedStock"
|
||||||
:selected-position="selectedPosition"
|
:selected-position="selectedPosition"
|
||||||
:connected="connected"
|
:connected="connected"
|
||||||
:order-quantity="orderQuantity"
|
:order-quantity="orderQuantity"
|
||||||
:estimated-amount="estimatedAmount"
|
: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-request-cash-estimate="canRequestCashEstimate"
|
||||||
:can-place-cash-order="canPlaceCashOrder"
|
:can-place-cash-order="canPlaceCashOrder"
|
||||||
:realtime-price-points="chartPricePoints"
|
:realtime-price-points="chartPricePoints"
|
||||||
|
:chart-notice="chartNotice"
|
||||||
:price-polling="pricePolling"
|
:price-polling="pricePolling"
|
||||||
:box-plot-style="boxPlotStyle"
|
|
||||||
:has-quote="hasQuote"
|
:has-quote="hasQuote"
|
||||||
@open-search="showSearch = true"
|
@open-search="showSearch = true"
|
||||||
@select-stock="selectStock"
|
@select-stock="selectStock"
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ const methodAllowed = (method: string) => {
|
|||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
root: 'grid gap-3',
|
root: 'grid gap-3',
|
||||||
limitGrid: 'grid grid-cols-3 gap-3',
|
limitGrid: 'grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3',
|
||||||
limitGridCompact: 'grid-cols-5',
|
limitGridCompact: 'xl:grid-cols-5',
|
||||||
label: 'grid gap-2 text-xs font-extrabold text-[#9aa0a9]',
|
label: 'grid gap-2 text-xs font-extrabold text-[#9aa0a9]',
|
||||||
input:
|
input:
|
||||||
'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition focus:border-[#a8c7fa]',
|
'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',
|
actions: 'mt-3 flex flex-wrap gap-2',
|
||||||
button:
|
button:
|
||||||
'min-h-9 rounded-full bg-[#263141] px-4 text-sm font-extrabold text-[#d3e3fd] transition hover:bg-[#303b4d]',
|
'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:
|
methodToggle:
|
||||||
'flex min-h-10 items-center gap-2 rounded-[16px] bg-[#111418] p-3 text-xs text-[#c3c7cf]',
|
'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',
|
checkbox: 'h-4 min-h-4 w-4',
|
||||||
@@ -128,7 +128,7 @@ const ui = {
|
|||||||
<input v-model.number="settings.maxTradesPerHour" :class="ui.input" type="number" min="0" />
|
<input v-model.number="settings.maxTradesPerHour" :class="ui.input" type="number" min="0" />
|
||||||
</label>
|
</label>
|
||||||
<label :class="ui.label">
|
<label :class="ui.label">
|
||||||
3時間 取引上限
|
6時間 取引上限
|
||||||
<input
|
<input
|
||||||
v-model.number="settings.maxTradesPer6Hours"
|
v-model.number="settings.maxTradesPer6Hours"
|
||||||
:class="ui.input"
|
:class="ui.input"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const props = withDefaults(
|
|||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
min?: string | number
|
min?: string | number
|
||||||
|
step?: string | number
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,7 @@ const props = withDefaults(
|
|||||||
readonly: false,
|
readonly: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
min: undefined,
|
min: undefined,
|
||||||
|
step: undefined,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -77,6 +79,7 @@ const controlClass =
|
|||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:type="type"
|
:type="type"
|
||||||
:min="min"
|
:min="min"
|
||||||
|
:step="step"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type {
|
|||||||
CashOrderMarket,
|
CashOrderMarket,
|
||||||
CashOrderMethod,
|
CashOrderMethod,
|
||||||
CashOrderPriceCondition,
|
CashOrderPriceCondition,
|
||||||
CashOrderTerm,
|
|
||||||
CashOrderTriggerZone,
|
CashOrderTriggerZone,
|
||||||
OrderKind,
|
OrderKind,
|
||||||
TradeSide,
|
TradeSide,
|
||||||
@@ -25,6 +24,8 @@ export const cashOrderAccountTypeOptions: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ label: '特定', value: 'specific' },
|
{ label: '特定', value: 'specific' },
|
||||||
{ label: '一般', value: 'general' },
|
{ label: '一般', value: 'general' },
|
||||||
|
{ label: 'NISA成長投資枠', value: 'growthInvestment' },
|
||||||
|
{ label: 'NISA', value: 'nisa' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const cashOrderMarketOptions: Array<{
|
export const cashOrderMarketOptions: Array<{
|
||||||
@@ -33,6 +34,9 @@ export const cashOrderMarketOptions: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ label: '自動', value: 'auto' },
|
{ label: '自動', value: 'auto' },
|
||||||
{ label: '東証', value: 'TKY' },
|
{ label: '東証', value: 'TKY' },
|
||||||
|
{ label: '名証', value: 'NGY' },
|
||||||
|
{ label: '福証', value: 'FKO' },
|
||||||
|
{ label: '札証', value: 'SPR' },
|
||||||
{ label: 'SOR', value: 'SOR' },
|
{ label: 'SOR', value: 'SOR' },
|
||||||
{ label: 'PTS', value: 'PTS' },
|
{ label: 'PTS', value: 'PTS' },
|
||||||
{ label: 'PTS(X)', value: 'PTX' },
|
{ label: 'PTS(X)', value: 'PTX' },
|
||||||
@@ -58,15 +62,6 @@ export const cashOrderPriceConditionOptions: Array<{
|
|||||||
{ label: '不成', value: 'funari' },
|
{ label: '不成', value: 'funari' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const cashOrderTermOptions: Array<{
|
|
||||||
label: string
|
|
||||||
value: CashOrderTerm
|
|
||||||
}> = [
|
|
||||||
{ label: '当日中', value: 'day' },
|
|
||||||
{ label: '今週中', value: 'week' },
|
|
||||||
{ label: '日付指定', value: 'date' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const cashOrderMethodOptions: Array<{
|
export const cashOrderMethodOptions: Array<{
|
||||||
label: string
|
label: string
|
||||||
value: CashOrderMethod
|
value: CashOrderMethod
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ const orderMethodLabel = (method: CashOrderMethod) =>
|
|||||||
method === 'normal' ? '通常' : method === 'stop' ? '逆指値' : 'OCO'
|
method === 'normal' ? '通常' : method === 'stop' ? '逆指値' : 'OCO'
|
||||||
|
|
||||||
const triggerZoneLabel = (zone: CashOrderTriggerZone) => (zone === 'above' ? '以上' : '以下')
|
const triggerZoneLabel = (zone: CashOrderTriggerZone) => (zone === 'above' ? '以上' : '以下')
|
||||||
|
|
||||||
|
const accountTypeLabel = (type: CashOrderAccountType) =>
|
||||||
|
({
|
||||||
|
specific: '特定',
|
||||||
|
general: '一般',
|
||||||
|
growthInvestment: 'NISA成長投資枠',
|
||||||
|
nisa: 'NISA',
|
||||||
|
})[type]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -80,7 +88,7 @@ const triggerZoneLabel = (zone: CashOrderTriggerZone) => (zone === 'above' ? '
|
|||||||
</div>
|
</div>
|
||||||
<div :class="ui.confirmRow">
|
<div :class="ui.confirmRow">
|
||||||
<dt>預り区分</dt>
|
<dt>預り区分</dt>
|
||||||
<dd>{{ accountType === 'specific' ? '特定' : '一般' }}</dd>
|
<dd>{{ accountTypeLabel(accountType) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div :class="ui.confirmRow">
|
<div :class="ui.confirmRow">
|
||||||
<dt>注文市場</dt>
|
<dt>注文市場</dt>
|
||||||
@@ -151,7 +159,7 @@ const triggerZoneLabel = (zone: CashOrderTriggerZone) => (zone === 'above' ? '
|
|||||||
</div>
|
</div>
|
||||||
<div :class="ui.confirmRow">
|
<div :class="ui.confirmRow">
|
||||||
<dt>預り区分</dt>
|
<dt>預り区分</dt>
|
||||||
<dd>{{ accountType === 'specific' ? '特定' : '一般' }}</dd>
|
<dd>{{ accountTypeLabel(accountType) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div :class="ui.confirmRow">
|
<div :class="ui.confirmRow">
|
||||||
<dt>注文市場</dt>
|
<dt>注文市場</dt>
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ban, KeyRound, Plug, RefreshCw, Save, Trash2 } from 'lucide-vue-next'
|
import {
|
||||||
|
Ban,
|
||||||
|
ChevronRight,
|
||||||
|
KeyRound,
|
||||||
|
Pencil,
|
||||||
|
Plug,
|
||||||
|
RefreshCw,
|
||||||
|
Save,
|
||||||
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
|
UserRoundCog,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { ApiKey, ApiKeySettings, SbiPasskey } from '../../api'
|
import type { ApiKey, ApiKeySettings, SbiPasskey } from '../../api'
|
||||||
import ApiKeyPolicyEditor from '../../components/ApiKeyPolicyEditor.vue'
|
import ApiKeyPolicyEditor from '../../components/ApiKeyPolicyEditor.vue'
|
||||||
import { ui } from '../../styles/ui'
|
import { ui } from '../../styles/ui'
|
||||||
@@ -27,16 +40,139 @@ const emit = defineEmits<{
|
|||||||
connect: []
|
connect: []
|
||||||
removeSbiPasskey: [id: string]
|
removeSbiPasskey: [id: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
type SettingsSection = 'api-keys' | 'passkeys'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const editingApiKey = ref<ApiKey | null>(null)
|
||||||
|
|
||||||
|
const activeSection = computed<SettingsSection>(() => {
|
||||||
|
return route.params.section === 'passkeys' ? 'passkeys' : 'api-keys'
|
||||||
|
})
|
||||||
|
|
||||||
|
const settingsGroups = [
|
||||||
|
{
|
||||||
|
label: 'アクセス',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'api-keys',
|
||||||
|
label: 'API キー',
|
||||||
|
description: '権限と取引上限を管理',
|
||||||
|
icon: KeyRound,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '証券口座',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'passkeys',
|
||||||
|
label: 'SBI パスキー',
|
||||||
|
description: '接続プロフィールを管理',
|
||||||
|
icon: UserRoundCog,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const openSection = (section: SettingsSection) => {
|
||||||
|
router.push(`/settings/${section}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionTitle = computed(() =>
|
||||||
|
activeSection.value === 'api-keys' ? 'API キー' : 'SBI パスキー',
|
||||||
|
)
|
||||||
|
|
||||||
|
const openApiKeyEditor = (key: ApiKey) => {
|
||||||
|
editingApiKey.value = {
|
||||||
|
...key,
|
||||||
|
allowedMethods: key.allowedMethods ? [...key.allowedMethods] : key.allowedMethods,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeApiKeyEditor = () => {
|
||||||
|
editingApiKey.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEditingApiKey = () => {
|
||||||
|
if (!editingApiKey.value) return
|
||||||
|
emit('saveApiKeySettings', editingApiKey.value)
|
||||||
|
closeApiKeyEditor()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section :class="ui.settingsLayout">
|
<section
|
||||||
<div :class="ui.apiLayout">
|
class="grid min-h-[calc(100dvh-9rem)] min-w-0 max-w-full gap-0 lg:grid-cols-[18rem_minmax(0,1fr)]"
|
||||||
<article :class="ui.panel">
|
>
|
||||||
<div :class="ui.panelHead">
|
<aside
|
||||||
|
class="min-w-0 border-b border-[#30343a] pb-4 lg:sticky lg:top-0 lg:h-fit lg:border-r lg:border-b-0 lg:pr-5 lg:pb-0"
|
||||||
|
aria-label="Settings"
|
||||||
|
>
|
||||||
|
<div class="grid gap-5">
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<h2 class="text-xl font-black text-[#e3e3e9]">設定</h2>
|
||||||
|
<p class="text-sm font-semibold text-[#9aa0a9]">用途ごとに管理項目を分けています。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="grid gap-5">
|
||||||
|
<section v-for="group in settingsGroups" :key="group.label" class="grid gap-2">
|
||||||
|
<p class="px-2 text-xs font-black text-[#747982]">{{ group.label }}</p>
|
||||||
|
<button
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.id"
|
||||||
|
:class="[
|
||||||
|
'grid min-h-16 grid-cols-[2.5rem_minmax(0,1fr)_auto] items-center gap-3 rounded-[18px] px-3 text-left transition hover:bg-[#22272e] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d3e3fd]',
|
||||||
|
activeSection === item.id ? 'bg-[#263141] text-[#d3e3fd]' : 'text-[#e3e3e9]',
|
||||||
|
]"
|
||||||
|
type="button"
|
||||||
|
@click="openSection(item.id)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'grid h-10 w-10 place-items-center rounded-full',
|
||||||
|
activeSection === item.id
|
||||||
|
? 'bg-[#a8c7fa] text-[#102033]'
|
||||||
|
: 'bg-[#111418] text-[#c3c7cf]',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" class="h-5 w-5" :stroke-width="2.4" />
|
||||||
|
</span>
|
||||||
|
<span class="grid min-w-0 gap-0.5">
|
||||||
|
<strong class="truncate text-sm font-black">{{ item.label }}</strong>
|
||||||
|
<small class="truncate text-xs font-semibold text-[#9aa0a9]">
|
||||||
|
{{ item.description }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<ChevronRight class="h-4 w-4 text-[#8f949d]" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="grid min-w-0 content-start gap-5 pt-5 lg:pl-7 lg:pt-0">
|
||||||
|
<header
|
||||||
|
class="flex flex-wrap items-center justify-between gap-3 border-b border-[#30343a] pb-5"
|
||||||
|
>
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<p :class="ui.eyebrow">Settings</p>
|
||||||
|
<h2 class="text-2xl font-black text-[#e3e3e9]">{{ sectionTitle }}</h2>
|
||||||
|
</div>
|
||||||
|
<button :class="ui.ghostButton" type="button" @click="emit('refresh')">
|
||||||
|
<RefreshCw class="h-4 w-4" aria-hidden="true" />
|
||||||
|
更新
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<template v-if="activeSection === 'api-keys'">
|
||||||
|
<article :class="[ui.panel, 'min-w-0 overflow-hidden']">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p :class="ui.eyebrow">Fine-grained</p>
|
<p :class="ui.eyebrow">Create</p>
|
||||||
<h2>API Key 発行</h2>
|
<h3 class="text-lg font-black">API Key 発行</h3>
|
||||||
</div>
|
</div>
|
||||||
<button :class="ui.primaryButton" type="button" @click="emit('addApiKey')">
|
<button :class="ui.primaryButton" type="button" @click="emit('addApiKey')">
|
||||||
<KeyRound class="h-4 w-4" aria-hidden="true" />
|
<KeyRound class="h-4 w-4" aria-hidden="true" />
|
||||||
@@ -48,30 +184,36 @@ const emit = defineEmits<{
|
|||||||
<input v-model="apiKeyLabel" :class="ui.input" placeholder="例: trade-bot-readonly" />
|
<input v-model="apiKeyLabel" :class="ui.input" placeholder="例: trade-bot-readonly" />
|
||||||
</label>
|
</label>
|
||||||
<ApiKeyPolicyEditor v-model="newApiKeySettings" permissions-open />
|
<ApiKeyPolicyEditor v-model="newApiKeySettings" permissions-open />
|
||||||
<textarea v-if="newApiToken" v-model="newApiToken" :class="ui.input" readonly></textarea>
|
<label v-if="newApiToken" :class="ui.label">
|
||||||
|
新しい API キー
|
||||||
|
<textarea
|
||||||
|
v-model="newApiToken"
|
||||||
|
:class="[ui.input, 'min-h-28 py-3']"
|
||||||
|
readonly
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article :class="ui.panel">
|
<article :class="[ui.panel, 'min-w-0 overflow-hidden']">
|
||||||
<div :class="ui.panelHead">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h2>発行済みキー</h2>
|
<div>
|
||||||
<button :class="ui.ghostButton" type="button" @click="emit('refresh')">
|
<p :class="ui.eyebrow">Active keys</p>
|
||||||
<RefreshCw class="h-4 w-4" aria-hidden="true" />
|
<h3 class="text-lg font-black">発行済みキー</h3>
|
||||||
更新
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div :class="ui.list">
|
</div>
|
||||||
|
<div v-if="apiKeys.length" :class="ui.list">
|
||||||
<div v-for="key in apiKeys" :key="key.id" :class="ui.keyRow">
|
<div v-for="key in apiKeys" :key="key.id" :class="ui.keyRow">
|
||||||
<div :class="ui.row">
|
<div :class="ui.row">
|
||||||
<span>{{ key.label }}</span>
|
<span class="min-w-0 truncate font-black">{{ key.label }}</span>
|
||||||
<div :class="ui.rowActions">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
:class="ui.primaryButton"
|
:class="ui.ghostButton"
|
||||||
type="button"
|
type="button"
|
||||||
title="Save settings"
|
title="Edit settings"
|
||||||
@click="emit('saveApiKeySettings', key)"
|
@click="openApiKeyEditor(key)"
|
||||||
>
|
>
|
||||||
<Save class="h-4 w-4" aria-hidden="true" />
|
<Pencil class="h-4 w-4" aria-hidden="true" />
|
||||||
保存
|
編集
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:class="ui.dangerButton"
|
:class="ui.dangerButton"
|
||||||
@@ -84,22 +226,23 @@ const emit = defineEmits<{
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ApiKeyPolicyEditor
|
|
||||||
:model-value="key"
|
|
||||||
compact
|
|
||||||
@update:model-value="Object.assign(key, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="rounded-[18px] border border-[#30343a] bg-[#111418] p-5 text-sm font-semibold text-[#9aa0a9]"
|
||||||
|
>
|
||||||
|
発行済みの API キーはありません。
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div :class="ui.apiLayout">
|
<template v-else>
|
||||||
<article :class="ui.panel">
|
<article :class="[ui.panel, 'min-w-0 overflow-hidden']">
|
||||||
<div :class="ui.panelHead">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p :class="ui.eyebrow">Profiles</p>
|
<p :class="ui.eyebrow">Profiles</p>
|
||||||
<h2>パスキー設定</h2>
|
<h3 class="text-lg font-black">パスキー追加</h3>
|
||||||
</div>
|
</div>
|
||||||
<button :class="ui.primaryButton" type="button" @click="emit('addSbiPasskey')">
|
<button :class="ui.primaryButton" type="button" @click="emit('addSbiPasskey')">
|
||||||
<Save class="h-4 w-4" aria-hidden="true" />
|
<Save class="h-4 w-4" aria-hidden="true" />
|
||||||
@@ -114,32 +257,40 @@ const emit = defineEmits<{
|
|||||||
パスキー JSON
|
パスキー JSON
|
||||||
<textarea
|
<textarea
|
||||||
v-model="sbiCredentialJson"
|
v-model="sbiCredentialJson"
|
||||||
:class="ui.input"
|
:class="[ui.input, 'min-h-48 py-3 font-mono text-xs']"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
placeholder="{ ... }"
|
placeholder="{ ... }"
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
<div :class="ui.ticketBox">
|
<div class="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||||
<label :class="ui.label">
|
<label :class="ui.label">
|
||||||
取引パスワード
|
取引パスワード
|
||||||
<input v-model="tradePassword" :class="ui.input" type="password" autocomplete="off" />
|
<input v-model="tradePassword" :class="ui.input" type="password" autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
<label :class="ui.label">
|
<label :class="ui.label">
|
||||||
デバイスキー
|
デバイスキー
|
||||||
<input v-model="sbiDeviceId" :class="ui.input" autocomplete="off" spellcheck="false" />
|
<input
|
||||||
|
v-model="sbiDeviceId"
|
||||||
|
:class="ui.input"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article :class="ui.panel">
|
<article :class="[ui.panel, 'min-w-0 overflow-hidden']">
|
||||||
<div :class="ui.panelHead">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h2>プロフィール切り替え</h2>
|
<div>
|
||||||
|
<p :class="ui.eyebrow">Connection</p>
|
||||||
|
<h3 class="text-lg font-black">プロフィール切り替え</h3>
|
||||||
|
</div>
|
||||||
<button :class="ui.ghostButton" type="button" @click="emit('connect')">
|
<button :class="ui.ghostButton" type="button" @click="emit('connect')">
|
||||||
<Plug class="h-4 w-4" aria-hidden="true" />
|
<Plug class="h-4 w-4" aria-hidden="true" />
|
||||||
接続
|
接続
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="ui.list">
|
<div v-if="sbiPasskeys.length" :class="ui.list">
|
||||||
<label v-for="passkey in sbiPasskeys" :key="passkey.id" :class="ui.profileRow">
|
<label v-for="passkey in sbiPasskeys" :key="passkey.id" :class="ui.profileRow">
|
||||||
<input
|
<input
|
||||||
v-model="selectedPasskeyId"
|
v-model="selectedPasskeyId"
|
||||||
@@ -147,9 +298,11 @@ const emit = defineEmits<{
|
|||||||
:value="passkey.id"
|
:value="passkey.id"
|
||||||
@change="emit('connect')"
|
@change="emit('connect')"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span class="grid min-w-0 gap-1">
|
||||||
<strong>{{ passkey.label }}</strong>
|
<strong class="truncate">{{ passkey.label }}</strong>
|
||||||
<small>{{ new Date(passkey.createdAt).toLocaleString() }}</small>
|
<small class="truncate text-[#8f949d]">
|
||||||
|
{{ new Date(passkey.createdAt).toLocaleString() }}
|
||||||
|
</small>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
:class="ui.dangerButton"
|
:class="ui.dangerButton"
|
||||||
@@ -162,7 +315,51 @@ const emit = defineEmits<{
|
|||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="grid gap-2 rounded-[18px] border border-[#30343a] bg-[#111418] p-5 text-sm font-semibold text-[#9aa0a9]"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center gap-2 text-[#c3c7cf]">
|
||||||
|
<ShieldCheck class="h-4 w-4" aria-hidden="true" />
|
||||||
|
保存済みプロフィールはありません。
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="editingApiKey"
|
||||||
|
class="fixed inset-0 z-30 grid place-items-center overflow-y-auto bg-[#101418]/75 p-4 backdrop-blur-sm"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
@click.self="closeApiKeyEditor"
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
class="grid max-h-[calc(100dvh-2rem)] w-full max-w-5xl gap-5 overflow-y-auto rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-6 shadow-2xl shadow-black/35"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="flex flex-wrap items-center justify-between gap-3 border-b border-[#33383f] pb-4"
|
||||||
|
>
|
||||||
|
<div class="grid min-w-0 gap-1">
|
||||||
|
<p :class="ui.eyebrow">Edit API key</p>
|
||||||
|
<h3 class="truncate text-xl font-black text-[#e3e3e9]">
|
||||||
|
{{ editingApiKey.label }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button :class="ui.ghostButton" type="button" @click="closeApiKeyEditor">
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
<button :class="ui.primaryButton" type="button" @click="saveEditingApiKey">
|
||||||
|
<Save class="h-4 w-4" aria-hidden="true" />
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ApiKeyPolicyEditor v-model="editingApiKey" permissions-open />
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,225 +1,719 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Chart from 'chart.js/auto'
|
import * as d3 from 'd3'
|
||||||
import { Minus, Plus, RotateCcw } from 'lucide-vue-next'
|
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import Spinner from '../../components/ui/Spinner.vue'
|
import Spinner from '../../components/ui/Spinner.vue'
|
||||||
import type { RealtimePricePoint } from '../../types/trading'
|
import type { ChartMode, ChartNotice, ChartRange, RealtimePricePoint } from '../../types/trading'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
points: RealtimePricePoint[]
|
points: RealtimePricePoint[]
|
||||||
stockName: string
|
stockName: string
|
||||||
active: boolean
|
active: boolean
|
||||||
|
mode: ChartMode
|
||||||
|
range: ChartRange
|
||||||
|
previousClose?: number
|
||||||
|
notice?: ChartNotice | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
type ChartPoint = RealtimePricePoint & {
|
||||||
const scroller = ref<HTMLDivElement | null>(null)
|
index: number
|
||||||
const zoomScale = ref(1)
|
date: Date
|
||||||
let chart: Chart<'line', number[], string> | undefined
|
}
|
||||||
let wasScrolledToEnd = true
|
|
||||||
|
|
||||||
const minChartWidth = 720
|
const root = ref<HTMLDivElement | null>(null)
|
||||||
|
const svg = ref<SVGSVGElement | null>(null)
|
||||||
|
const gridLayer = ref<SVGGElement | null>(null)
|
||||||
|
const xAxisLayer = ref<SVGGElement | null>(null)
|
||||||
|
const yAxisLayer = ref<SVGGElement | null>(null)
|
||||||
|
const areaPath = ref<SVGPathElement | null>(null)
|
||||||
|
const linePath = ref<SVGPathElement | null>(null)
|
||||||
|
const previousCloseLine = ref<SVGLineElement | null>(null)
|
||||||
|
const dotsLayer = ref<SVGGElement | null>(null)
|
||||||
|
const candlesLayer = ref<SVGGElement | null>(null)
|
||||||
|
const hoverLayer = ref<SVGGElement | null>(null)
|
||||||
|
const hoverLine = ref<SVGLineElement | null>(null)
|
||||||
|
const hoverDot = ref<SVGCircleElement | null>(null)
|
||||||
|
const clipRect = ref<SVGRectElement | null>(null)
|
||||||
|
const zoomScale = ref(1)
|
||||||
|
const size = ref({ width: 0, height: 0 })
|
||||||
|
const tooltip = ref({
|
||||||
|
visible: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
price: '',
|
||||||
|
date: '',
|
||||||
|
detail: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | undefined
|
||||||
|
let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown> | undefined
|
||||||
|
let zoomTransform = d3.zoomIdentity
|
||||||
|
let syncingZoom = false
|
||||||
|
let followsLatest = true
|
||||||
|
let latestXScale: d3.ScaleTime<number, number> | undefined
|
||||||
|
let latestYScale: d3.ScaleLinear<number, number> | undefined
|
||||||
|
|
||||||
|
const clipId = `realtime-price-clip-${Math.random().toString(36).slice(2)}`
|
||||||
|
const margin = { top: 12, right: 18, bottom: 28, left: 48 }
|
||||||
const minZoom = 0.6
|
const minZoom = 0.6
|
||||||
const maxZoom = 3
|
const maxZoom = 6
|
||||||
const zoomStep = 0.2
|
const zoomStep = 0.2
|
||||||
const basePointSpacing = 10
|
const basePointSpacing = 10
|
||||||
const pointSpacing = computed(() => basePointSpacing * zoomScale.value)
|
const dayMs = 24 * 60 * 60 * 1000
|
||||||
|
const tolerancePx = 10
|
||||||
|
const offscreenPointBuffer = 4
|
||||||
|
const minXAxisTickSpacing = 72
|
||||||
|
|
||||||
const labelFor = (point: RealtimePricePoint) => {
|
const chartPoints = computed<ChartPoint[]>(() =>
|
||||||
const date = new Date(point.at)
|
props.points
|
||||||
if (date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0) {
|
.map((point, index) => ({ ...point, index, date: new Date(point.at) }))
|
||||||
|
.filter((point) => Number.isFinite(point.price) && !Number.isNaN(point.date.getTime())),
|
||||||
|
)
|
||||||
|
|
||||||
|
const plotWidth = computed(() => Math.max(1, size.value.width - margin.left - margin.right))
|
||||||
|
const plotHeight = computed(() => Math.max(1, size.value.height - margin.top - margin.bottom))
|
||||||
|
|
||||||
|
const labelFor = (point: ChartPoint) => {
|
||||||
|
if (props.range === '1D') {
|
||||||
|
return new Intl.DateTimeFormat('ja-JP', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(point.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
point.date.getHours() === 0 &&
|
||||||
|
point.date.getMinutes() === 0 &&
|
||||||
|
point.date.getSeconds() === 0
|
||||||
|
) {
|
||||||
return new Intl.DateTimeFormat('ja-JP', {
|
return new Intl.DateTimeFormat('ja-JP', {
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
}).format(date)
|
}).format(point.date)
|
||||||
}
|
}
|
||||||
return new Intl.DateTimeFormat('ja-JP', {
|
return new Intl.DateTimeFormat('ja-JP', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
}).format(point.date)
|
||||||
}).format(date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const labels = computed(() => props.points.map(labelFor))
|
const closePrice = (point: ChartPoint) => point.close ?? point.price
|
||||||
|
|
||||||
const prices = computed(() => props.points.map((point) => point.price))
|
const previousCloseValue = computed(() =>
|
||||||
|
props.range === '1D' && Number.isFinite(props.previousClose) && props.previousClose
|
||||||
|
? props.previousClose
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
|
||||||
const chartWidth = computed(() => {
|
const ohlcFor = (point: ChartPoint) => {
|
||||||
const width = Math.max(minChartWidth, props.points.length * pointSpacing.value + 72)
|
const close = closePrice(point)
|
||||||
return `max(100%, ${width}px)`
|
const open = point.open ?? close
|
||||||
|
const high = Math.max(point.high ?? close, open, close)
|
||||||
|
const low = Math.min(point.low ?? close, open, close)
|
||||||
|
return { open, high, low, close }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipDateFor = (point: ChartPoint) => {
|
||||||
|
const hasTime =
|
||||||
|
point.date.getHours() !== 0 || point.date.getMinutes() !== 0 || point.date.getSeconds() !== 0
|
||||||
|
return new Intl.DateTimeFormat('ja-JP', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
...(hasTime
|
||||||
|
? {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}).format(point.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipStyle = computed(() => {
|
||||||
|
const width = 152
|
||||||
|
const pad = 8
|
||||||
|
const left = Math.min(
|
||||||
|
Math.max(pad, tooltip.value.x + 12),
|
||||||
|
Math.max(pad, size.value.width - width - pad),
|
||||||
|
)
|
||||||
|
const top =
|
||||||
|
tooltip.value.y > 76
|
||||||
|
? Math.max(pad, tooltip.value.y - 70)
|
||||||
|
: Math.min(size.value.height - 64, tooltip.value.y + 14)
|
||||||
|
return {
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${top}px`,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const rememberScrollPosition = () => {
|
const timeDomainMs = (): [number, number] => {
|
||||||
const element = scroller.value
|
const points = chartPoints.value
|
||||||
if (!element) return
|
const start = d3.min(points, (point) => point.date.getTime()) ?? 0
|
||||||
wasScrolledToEnd =
|
const end = d3.max(points, (point) => point.date.getTime()) ?? start + dayMs
|
||||||
element.scrollLeft + element.clientWidth >= element.scrollWidth - pointSpacing.value
|
return end === start ? [start, start + dayMs] : [start, end]
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToEndIfNeeded = async () => {
|
const baseContentWidth = () => {
|
||||||
if (!wasScrolledToEnd) return
|
const points = chartPoints.value
|
||||||
await nextTick()
|
return Math.max(plotWidth.value, Math.max(1, points.length - 1) * basePointSpacing)
|
||||||
const element = scroller.value
|
|
||||||
if (!element) return
|
|
||||||
element.scrollLeft = element.scrollWidth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateChart = () => {
|
const basePixelsPerMs = () => {
|
||||||
if (!chart) return
|
const [start, end] = timeDomainMs()
|
||||||
rememberScrollPosition()
|
const duration = end - start
|
||||||
chart.data.labels = labels.value
|
return duration > 0 ? baseContentWidth() / duration : basePointSpacing / dayMs
|
||||||
const dataset = chart.data.datasets[0]
|
}
|
||||||
if (dataset) dataset.data = prices.value
|
|
||||||
chart.update('none')
|
const contentWidth = (k = zoomTransform.k) => {
|
||||||
chart.resize()
|
return Math.max(plotWidth.value, baseContentWidth() * k)
|
||||||
void scrollToEndIfNeeded()
|
}
|
||||||
|
|
||||||
|
const minTranslateX = (k = zoomTransform.k) => Math.min(0, plotWidth.value - contentWidth(k))
|
||||||
|
|
||||||
|
const zoomTransformWith = (k: number, x: number) => d3.zoomIdentity.translate(x, 0).scale(k)
|
||||||
|
|
||||||
|
const clampTransform = (transform: d3.ZoomTransform) => {
|
||||||
|
const k = Math.min(maxZoom, Math.max(minZoom, transform.k))
|
||||||
|
const minX = minTranslateX(k)
|
||||||
|
const x = Math.min(0, Math.max(minX, transform.x))
|
||||||
|
return zoomTransformWith(k, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLatestVisible = (transform = zoomTransform) =>
|
||||||
|
contentWidth(transform.k) + transform.x <= plotWidth.value + tolerancePx
|
||||||
|
|
||||||
|
const syncZoomBehavior = (transform: d3.ZoomTransform) => {
|
||||||
|
if (!svg.value || !zoomBehavior) return
|
||||||
|
syncingZoom = true
|
||||||
|
d3.select(svg.value).call(zoomBehavior.transform, transform)
|
||||||
|
syncingZoom = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyTransform = (transform: d3.ZoomTransform, sync = true) => {
|
||||||
|
zoomTransform = clampTransform(transform)
|
||||||
|
zoomScale.value = Number(zoomTransform.k.toFixed(2))
|
||||||
|
followsLatest = isLatestVisible(zoomTransform)
|
||||||
|
renderChart()
|
||||||
|
if (sync) syncZoomBehavior(zoomTransform)
|
||||||
|
}
|
||||||
|
|
||||||
|
const alignLatest = (k = zoomTransform.k, sync = true) => {
|
||||||
|
followsLatest = true
|
||||||
|
applyTransform(zoomTransformWith(k, minTranslateX(k)), sync)
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleTickIndexes = () => {
|
||||||
|
if (!latestXScale) return [] as Date[]
|
||||||
|
const maxTicks = Math.max(2, Math.floor(plotWidth.value / minXAxisTickSpacing))
|
||||||
|
const tickDates = latestXScale.ticks(maxTicks).filter((date) => {
|
||||||
|
const x = latestXScale?.(date) ?? 0
|
||||||
|
return x >= margin.left && x <= margin.left + plotWidth.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleTicks: Date[] = []
|
||||||
|
let previousX = Number.NEGATIVE_INFINITY
|
||||||
|
for (const date of tickDates) {
|
||||||
|
const x = latestXScale(date)
|
||||||
|
if (x - previousX < minXAxisTickSpacing) continue
|
||||||
|
visibleTicks.push(date)
|
||||||
|
previousX = x
|
||||||
|
}
|
||||||
|
return visibleTicks
|
||||||
|
}
|
||||||
|
|
||||||
|
const visiblePointRange = (points: ChartPoint[]) => {
|
||||||
|
const [domainStart] = timeDomainMs()
|
||||||
|
const pixelsPerMs = basePixelsPerMs() * zoomTransform.k
|
||||||
|
const rawStartMs = domainStart + -zoomTransform.x / pixelsPerMs
|
||||||
|
const rawEndMs = domainStart + (plotWidth.value - zoomTransform.x) / pixelsPerMs
|
||||||
|
const rawStart = d3
|
||||||
|
.bisector<ChartPoint, number>((point) => point.date.getTime())
|
||||||
|
.left(points, rawStartMs)
|
||||||
|
const rawEnd = d3
|
||||||
|
.bisector<ChartPoint, number>((point) => point.date.getTime())
|
||||||
|
.right(points, rawEndMs)
|
||||||
|
const start = Math.max(0, Math.min(points.length - 1, rawStart - offscreenPointBuffer))
|
||||||
|
const end = Math.max(start, Math.min(points.length - 1, rawEnd + offscreenPointBuffer))
|
||||||
|
return { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderChart = () => {
|
||||||
|
const points = chartPoints.value
|
||||||
|
const width = size.value.width
|
||||||
|
const height = size.value.height
|
||||||
|
if (
|
||||||
|
!svg.value ||
|
||||||
|
!gridLayer.value ||
|
||||||
|
!xAxisLayer.value ||
|
||||||
|
!yAxisLayer.value ||
|
||||||
|
!areaPath.value ||
|
||||||
|
!linePath.value ||
|
||||||
|
!previousCloseLine.value ||
|
||||||
|
!dotsLayer.value ||
|
||||||
|
!candlesLayer.value ||
|
||||||
|
!hoverLayer.value ||
|
||||||
|
!clipRect.value ||
|
||||||
|
width <= 0 ||
|
||||||
|
height <= 0
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d3.select(clipRect.value)
|
||||||
|
.attr('x', margin.left)
|
||||||
|
.attr('y', margin.top)
|
||||||
|
.attr('width', plotWidth.value)
|
||||||
|
.attr('height', plotHeight.value)
|
||||||
|
|
||||||
|
if (!points.length) {
|
||||||
|
d3.select(areaPath.value).attr('d', '')
|
||||||
|
d3.select(linePath.value).attr('d', '')
|
||||||
|
d3.select(previousCloseLine.value).attr('display', 'none')
|
||||||
|
d3.select(dotsLayer.value).selectAll('circle').remove()
|
||||||
|
d3.select(candlesLayer.value).selectAll('*').remove()
|
||||||
|
hideHover()
|
||||||
|
d3.select(gridLayer.value).selectAll('*').remove()
|
||||||
|
d3.select(xAxisLayer.value).selectAll('*').remove()
|
||||||
|
d3.select(yAxisLayer.value).selectAll('*').remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const prices =
|
||||||
|
props.mode === 'box'
|
||||||
|
? points.flatMap((point) => {
|
||||||
|
const ohlc = ohlcFor(point)
|
||||||
|
return [ohlc.open, ohlc.high, ohlc.low, ohlc.close]
|
||||||
|
})
|
||||||
|
: points.map(closePrice)
|
||||||
|
if (previousCloseValue.value) prices.push(previousCloseValue.value)
|
||||||
|
const minPrice = d3.min(prices) ?? 0
|
||||||
|
const maxPrice = d3.max(prices) ?? 1
|
||||||
|
const padding = Math.max(1, (maxPrice - minPrice) * 0.08)
|
||||||
|
const yScale = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([minPrice - padding, maxPrice + padding])
|
||||||
|
.nice()
|
||||||
|
.range([margin.top + plotHeight.value, margin.top])
|
||||||
|
|
||||||
|
const [domainStart, domainEnd] = timeDomainMs()
|
||||||
|
const xScale = d3
|
||||||
|
.scaleTime()
|
||||||
|
.domain([new Date(domainStart), new Date(domainEnd)])
|
||||||
|
.range([margin.left + zoomTransform.x, margin.left + contentWidth() + zoomTransform.x])
|
||||||
|
latestXScale = xScale
|
||||||
|
latestYScale = yScale
|
||||||
|
|
||||||
|
const line = d3
|
||||||
|
.line<ChartPoint>()
|
||||||
|
.defined((point) => Number.isFinite(closePrice(point)))
|
||||||
|
.x((point) => xScale(point.date))
|
||||||
|
.y((point) => yScale(closePrice(point)))
|
||||||
|
.curve(d3.curveMonotoneX)
|
||||||
|
|
||||||
|
const area = d3
|
||||||
|
.area<ChartPoint>()
|
||||||
|
.defined((point) => Number.isFinite(closePrice(point)))
|
||||||
|
.x((point) => xScale(point.date))
|
||||||
|
.y0(margin.top + plotHeight.value)
|
||||||
|
.y1((point) => yScale(closePrice(point)))
|
||||||
|
.curve(d3.curveMonotoneX)
|
||||||
|
|
||||||
|
const { start, end } = visiblePointRange(points)
|
||||||
|
const visiblePoints = points.slice(start, end + 1)
|
||||||
|
|
||||||
|
d3.select(areaPath.value).attr('d', props.mode === 'line' ? (area(visiblePoints) ?? '') : '')
|
||||||
|
d3.select(linePath.value).attr('d', props.mode === 'line' ? (line(visiblePoints) ?? '') : '')
|
||||||
|
|
||||||
|
d3.select(dotsLayer.value)
|
||||||
|
.selectAll<SVGCircleElement, ChartPoint>('circle')
|
||||||
|
.data(props.mode === 'line' ? visiblePoints : [], (point) => point.at)
|
||||||
|
.join(
|
||||||
|
(enter) =>
|
||||||
|
enter
|
||||||
|
.append('circle')
|
||||||
|
.attr('r', 2.5)
|
||||||
|
.attr('fill', '#d3e3fd')
|
||||||
|
.attr('stroke', '#40dba2')
|
||||||
|
.attr('stroke-width', 1.6),
|
||||||
|
(update) => update,
|
||||||
|
(exit) => exit.remove(),
|
||||||
|
)
|
||||||
|
.attr('cx', (point) => xScale(point.date))
|
||||||
|
.attr('cy', (point) => yScale(closePrice(point)))
|
||||||
|
|
||||||
|
const previousCloseY = previousCloseValue.value ? yScale(previousCloseValue.value) : null
|
||||||
|
d3.select(previousCloseLine.value)
|
||||||
|
.attr('display', previousCloseY === null ? 'none' : null)
|
||||||
|
.attr('x1', margin.left)
|
||||||
|
.attr('x2', margin.left + plotWidth.value)
|
||||||
|
.attr('y1', previousCloseY ?? 0)
|
||||||
|
.attr('y2', previousCloseY ?? 0)
|
||||||
|
|
||||||
|
const candleGroups = d3
|
||||||
|
.select(candlesLayer.value)
|
||||||
|
.selectAll<SVGGElement, ChartPoint>('g')
|
||||||
|
.data(props.mode === 'box' ? visiblePoints : [], (point) => point.at)
|
||||||
|
.join(
|
||||||
|
(enter) => {
|
||||||
|
const group = enter.append('g')
|
||||||
|
group
|
||||||
|
.append('line')
|
||||||
|
.attr('class', 'wick')
|
||||||
|
.attr('stroke-width', 1.4)
|
||||||
|
.attr('stroke-linecap', 'round')
|
||||||
|
group.append('rect').attr('class', 'body').attr('rx', 1.5)
|
||||||
|
return group
|
||||||
|
},
|
||||||
|
(update) => update,
|
||||||
|
(exit) => exit.remove(),
|
||||||
|
)
|
||||||
|
|
||||||
|
candleGroups.each(function (point) {
|
||||||
|
const ohlc = ohlcFor(point)
|
||||||
|
const x = xScale(point.date)
|
||||||
|
const pointIndex = points.indexOf(point)
|
||||||
|
const previous = points[pointIndex - 1]
|
||||||
|
const next = points[pointIndex + 1]
|
||||||
|
const previousGap = previous ? point.date.getTime() - previous.date.getTime() : undefined
|
||||||
|
const nextGap = next ? next.date.getTime() - point.date.getTime() : undefined
|
||||||
|
const gap = Math.min(
|
||||||
|
...(previousGap && nextGap ? [previousGap, nextGap] : [previousGap ?? nextGap ?? dayMs]),
|
||||||
|
)
|
||||||
|
const candleWidth = Math.max(2, Math.min(14, gap * basePixelsPerMs() * zoomTransform.k * 0.62))
|
||||||
|
const yOpen = yScale(ohlc.open)
|
||||||
|
const yClose = yScale(ohlc.close)
|
||||||
|
const yHigh = yScale(ohlc.high)
|
||||||
|
const yLow = yScale(ohlc.low)
|
||||||
|
const rising = ohlc.close >= ohlc.open
|
||||||
|
const color = rising ? '#40dba2' : '#ffb4ab'
|
||||||
|
const bodyY = Math.min(yOpen, yClose)
|
||||||
|
const bodyHeight = Math.max(2, Math.abs(yClose - yOpen))
|
||||||
|
|
||||||
|
d3.select(this)
|
||||||
|
.select<SVGLineElement>('line.wick')
|
||||||
|
.attr('x1', x)
|
||||||
|
.attr('x2', x)
|
||||||
|
.attr('y1', yHigh)
|
||||||
|
.attr('y2', yLow)
|
||||||
|
.attr('stroke', color)
|
||||||
|
|
||||||
|
d3.select(this)
|
||||||
|
.select<SVGRectElement>('rect.body')
|
||||||
|
.attr('x', x - candleWidth / 2)
|
||||||
|
.attr('y', bodyY)
|
||||||
|
.attr('width', candleWidth)
|
||||||
|
.attr('height', bodyHeight)
|
||||||
|
.attr('fill', rising ? 'rgba(64, 219, 162, 0.28)' : 'rgba(255, 180, 171, 0.28)')
|
||||||
|
.attr('stroke', color)
|
||||||
|
.attr('stroke-width', 1.4)
|
||||||
|
})
|
||||||
|
|
||||||
|
const yGridAxis = d3
|
||||||
|
.axisLeft(yScale)
|
||||||
|
.ticks(5)
|
||||||
|
.tickSize(-plotWidth.value)
|
||||||
|
.tickFormat(() => '')
|
||||||
|
d3.select(gridLayer.value)
|
||||||
|
.attr('transform', `translate(${margin.left}, 0)`)
|
||||||
|
.call(yGridAxis)
|
||||||
|
.call((selection) => {
|
||||||
|
selection.select('.domain').remove()
|
||||||
|
selection.selectAll('line').attr('stroke', 'rgba(143, 148, 157, 0.12)')
|
||||||
|
})
|
||||||
|
|
||||||
|
const yAxis = d3
|
||||||
|
.axisLeft(yScale)
|
||||||
|
.ticks(5)
|
||||||
|
.tickSize(0)
|
||||||
|
.tickPadding(8)
|
||||||
|
.tickFormat((value) => Number(value).toLocaleString('ja-JP'))
|
||||||
|
d3.select(yAxisLayer.value)
|
||||||
|
.attr('transform', `translate(${margin.left}, 0)`)
|
||||||
|
.call(yAxis)
|
||||||
|
.call((selection) => {
|
||||||
|
selection.select('.domain').remove()
|
||||||
|
selection.selectAll('text').attr('fill', '#8f949d').attr('font-size', 12)
|
||||||
|
})
|
||||||
|
|
||||||
|
const tickDates = visibleTickIndexes()
|
||||||
|
const xAxis = d3
|
||||||
|
.axisBottom(xScale)
|
||||||
|
.tickValues(tickDates)
|
||||||
|
.tickSize(0)
|
||||||
|
.tickPadding(8)
|
||||||
|
.tickFormat((value) => {
|
||||||
|
const date = value instanceof Date ? value : new Date(Number(value))
|
||||||
|
return labelFor({
|
||||||
|
at: date.toISOString(),
|
||||||
|
date,
|
||||||
|
index: 0,
|
||||||
|
price: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
d3.select(xAxisLayer.value)
|
||||||
|
.attr('transform', `translate(0, ${margin.top + plotHeight.value})`)
|
||||||
|
.call(xAxis)
|
||||||
|
.call((selection) => {
|
||||||
|
selection.select('.domain').remove()
|
||||||
|
selection.selectAll('text').attr('fill', '#8f949d').attr('font-size', 12)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tooltip.value.visible) {
|
||||||
|
const hovered = nearestPointAt(tooltip.value.x)
|
||||||
|
if (hovered) showHover(hovered)
|
||||||
|
else hideHover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearestPointAt = (x: number) => {
|
||||||
|
if (!latestXScale) return undefined
|
||||||
|
const time = latestXScale.invert(x).getTime()
|
||||||
|
return d3.least(chartPoints.value, (point) => Math.abs(point.date.getTime() - time))
|
||||||
|
}
|
||||||
|
|
||||||
|
const showHover = (point: ChartPoint) => {
|
||||||
|
if (!latestXScale || !latestYScale || !hoverLayer.value || !hoverLine.value || !hoverDot.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
const x = latestXScale(point.date)
|
||||||
|
const ohlc = ohlcFor(point)
|
||||||
|
const y = latestYScale(props.mode === 'box' ? ohlc.close : closePrice(point))
|
||||||
|
if (x < margin.left || x > margin.left + plotWidth.value) {
|
||||||
|
hideHover()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.value = {
|
||||||
|
visible: true,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
price: `${closePrice(point).toLocaleString('ja-JP')}円`,
|
||||||
|
date: tooltipDateFor(point),
|
||||||
|
detail:
|
||||||
|
props.mode === 'box'
|
||||||
|
? `始 ${ohlc.open.toLocaleString('ja-JP')} / 高 ${ohlc.high.toLocaleString('ja-JP')} / 安 ${ohlc.low.toLocaleString('ja-JP')}`
|
||||||
|
: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
d3.select(hoverLayer.value).style('opacity', 1)
|
||||||
|
d3.select(hoverLine.value)
|
||||||
|
.transition()
|
||||||
|
.duration(120)
|
||||||
|
.ease(d3.easeCubicOut)
|
||||||
|
.attr('x1', x)
|
||||||
|
.attr('x2', x)
|
||||||
|
.attr('y1', margin.top)
|
||||||
|
.attr('y2', margin.top + plotHeight.value)
|
||||||
|
d3.select(hoverDot.value)
|
||||||
|
.transition()
|
||||||
|
.duration(120)
|
||||||
|
.ease(d3.easeCubicOut)
|
||||||
|
.attr('cx', x)
|
||||||
|
.attr('cy', y)
|
||||||
|
.attr('r', 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideHover = () => {
|
||||||
|
tooltip.value = { ...tooltip.value, visible: false }
|
||||||
|
if (!hoverLayer.value || !hoverDot.value) return
|
||||||
|
d3.select(hoverLayer.value).transition().duration(120).style('opacity', 0)
|
||||||
|
d3.select(hoverDot.value).transition().duration(120).attr('r', 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setZoomScale = (value: number) => {
|
const setZoomScale = (value: number) => {
|
||||||
rememberScrollPosition()
|
const k = Math.min(maxZoom, Math.max(minZoom, value))
|
||||||
zoomScale.value = Math.min(maxZoom, Math.max(minZoom, value))
|
const center = plotWidth.value / 2
|
||||||
|
const contentCenter = (center - zoomTransform.x) / zoomTransform.k
|
||||||
|
applyTransform(zoomTransformWith(k, center - contentCenter * k))
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoomOut = () => setZoomScale(zoomScale.value - zoomStep)
|
const zoomOut = () => setZoomScale(zoomScale.value - zoomStep)
|
||||||
|
|
||||||
const zoomIn = () => setZoomScale(zoomScale.value + zoomStep)
|
const zoomIn = () => setZoomScale(zoomScale.value + zoomStep)
|
||||||
|
|
||||||
const resetZoom = () => setZoomScale(1)
|
const resetZoom = () => alignLatest(1)
|
||||||
|
|
||||||
const setZoomScaleFromInput = (event: Event) => {
|
defineExpose({
|
||||||
const value = Number((event.target as HTMLInputElement).value)
|
resetZoom,
|
||||||
setZoomScale(value)
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizedWheelDelta = (value: number, event: WheelEvent) => {
|
||||||
|
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) return value * 16
|
||||||
|
if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) return value * plotWidth.value
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomAround = (k: number, localX: number) => {
|
||||||
|
const nextK = Math.min(maxZoom, Math.max(minZoom, k))
|
||||||
|
const center = Math.min(plotWidth.value, Math.max(0, localX - margin.left))
|
||||||
|
const contentCenter = (center - zoomTransform.x) / zoomTransform.k
|
||||||
|
applyTransform(zoomTransformWith(nextK, center - contentCenter * nextK))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWheel = (event: WheelEvent) => {
|
const handleWheel = (event: WheelEvent) => {
|
||||||
if (!event.ctrlKey && !event.metaKey) return
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setZoomScale(zoomScale.value + (event.deltaY < 0 ? zoomStep : -zoomStep))
|
hideHover()
|
||||||
|
|
||||||
|
const deltaX = normalizedWheelDelta(event.deltaX, event)
|
||||||
|
const deltaY = normalizedWheelDelta(event.deltaY, event)
|
||||||
|
const panDelta = event.shiftKey && !deltaX ? deltaY : deltaX
|
||||||
|
|
||||||
|
if ((event.shiftKey && panDelta) || Math.abs(panDelta) > Math.abs(deltaY)) {
|
||||||
|
applyTransform(zoomTransformWith(zoomTransform.k, zoomTransform.x - panDelta))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deltaY) return
|
||||||
|
const direction = deltaY < 0 ? 1 : -1
|
||||||
|
const [x] = svg.value ? d3.pointer(event, svg.value) : [event.offsetX]
|
||||||
|
zoomAround(zoomTransform.k + direction * zoomStep, x)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
if (!canvas.value) return
|
if (!svg.value || event.buttons) {
|
||||||
chart = new Chart(canvas.value, {
|
hideHover()
|
||||||
type: 'line',
|
return
|
||||||
data: {
|
}
|
||||||
labels: labels.value,
|
const [x, y] = d3.pointer(event, svg.value)
|
||||||
datasets: [
|
const insidePlot =
|
||||||
{
|
x >= margin.left &&
|
||||||
data: prices.value,
|
x <= margin.left + plotWidth.value &&
|
||||||
borderColor: '#40dba2',
|
y >= margin.top &&
|
||||||
backgroundColor: 'rgba(64, 219, 162, 0.14)',
|
y <= margin.top + plotHeight.value
|
||||||
borderWidth: 2,
|
if (!insidePlot) {
|
||||||
fill: true,
|
hideHover()
|
||||||
pointBackgroundColor: '#d3e3fd',
|
return
|
||||||
pointBorderColor: '#40dba2',
|
}
|
||||||
pointRadius: 2.5,
|
const point = nearestPointAt(x)
|
||||||
pointHoverRadius: 4,
|
if (point) showHover(point)
|
||||||
tension: 0.25,
|
}
|
||||||
},
|
|
||||||
],
|
onMounted(async () => {
|
||||||
},
|
await nextTick()
|
||||||
options: {
|
if (!root.value || !svg.value) return
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
animation: false,
|
const rect = entries[0]?.contentRect
|
||||||
plugins: {
|
if (!rect) return
|
||||||
legend: { display: false },
|
size.value = { width: rect.width, height: rect.height }
|
||||||
tooltip: {
|
if (followsLatest) alignLatest(zoomTransform.k)
|
||||||
displayColors: false,
|
else applyTransform(zoomTransform)
|
||||||
callbacks: {
|
|
||||||
label: (item) => `${(item.parsed.y ?? 0).toLocaleString('ja-JP')}円`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
grid: { color: 'rgba(143, 148, 157, 0.12)' },
|
|
||||||
ticks: { color: '#8f949d', maxTicksLimit: 5 },
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
grid: { color: 'rgba(143, 148, 157, 0.12)' },
|
|
||||||
ticks: {
|
|
||||||
color: '#8f949d',
|
|
||||||
callback: (value) =>
|
|
||||||
typeof value === 'number' ? value.toLocaleString('ja-JP') : value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
void scrollToEndIfNeeded()
|
resizeObserver.observe(root.value)
|
||||||
|
|
||||||
|
zoomBehavior = d3
|
||||||
|
.zoom<SVGSVGElement, unknown>()
|
||||||
|
.scaleExtent([minZoom, maxZoom])
|
||||||
|
.filter((event) => event.type !== 'wheel' && !event.button && event.type !== 'dblclick')
|
||||||
|
.on('zoom', (event) => {
|
||||||
|
if (syncingZoom) return
|
||||||
|
applyTransform(event.transform, false)
|
||||||
|
syncZoomBehavior(zoomTransform)
|
||||||
|
})
|
||||||
|
|
||||||
|
d3.select(svg.value).call(zoomBehavior).on('dblclick.zoom', null)
|
||||||
|
alignLatest(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.points, updateChart, { deep: true })
|
watch(
|
||||||
watch(chartWidth, () => {
|
() => props.points,
|
||||||
chart?.resize()
|
() => {
|
||||||
void scrollToEndIfNeeded()
|
if (followsLatest) alignLatest(zoomTransform.k)
|
||||||
})
|
else applyTransform(zoomTransform)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.mode, props.range, props.previousClose],
|
||||||
|
() => {
|
||||||
|
hideHover()
|
||||||
|
renderChart()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
chart?.destroy()
|
resizeObserver?.disconnect()
|
||||||
chart = undefined
|
if (svg.value) d3.select(svg.value).on('.zoom', null)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative h-full w-full">
|
<div ref="root" class="relative h-full w-full">
|
||||||
<div
|
<svg
|
||||||
ref="scroller"
|
ref="svg"
|
||||||
class="h-full w-full overflow-x-auto overflow-y-hidden"
|
class="h-full w-full cursor-grab touch-none select-none active:cursor-grabbing"
|
||||||
@scroll="rememberScrollPosition"
|
|
||||||
@wheel="handleWheel"
|
|
||||||
>
|
|
||||||
<div class="h-full" :style="{ width: chartWidth }">
|
|
||||||
<canvas
|
|
||||||
ref="canvas"
|
|
||||||
class="h-full w-full"
|
|
||||||
:aria-label="`${stockName} realtime price chart`"
|
:aria-label="`${stockName} realtime price chart`"
|
||||||
></canvas>
|
role="img"
|
||||||
</div>
|
@wheel="handleWheel"
|
||||||
</div>
|
@pointermove="handlePointerMove"
|
||||||
|
@pointerleave="hideHover"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath :id="clipId">
|
||||||
|
<rect ref="clipRect"></rect>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g ref="gridLayer"></g>
|
||||||
|
<g ref="yAxisLayer"></g>
|
||||||
|
<g ref="xAxisLayer"></g>
|
||||||
|
<path :clip-path="`url(#${clipId})`" ref="areaPath" fill="rgba(64, 219, 162, 0.14)"></path>
|
||||||
|
<path
|
||||||
|
:clip-path="`url(#${clipId})`"
|
||||||
|
ref="linePath"
|
||||||
|
fill="none"
|
||||||
|
stroke="#40dba2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2.2"
|
||||||
|
></path>
|
||||||
|
<line
|
||||||
|
ref="previousCloseLine"
|
||||||
|
stroke="#ffcf6e"
|
||||||
|
stroke-dasharray="5 5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="1.2"
|
||||||
|
></line>
|
||||||
|
<g :clip-path="`url(#${clipId})`" ref="candlesLayer"></g>
|
||||||
|
<g :clip-path="`url(#${clipId})`" ref="dotsLayer"></g>
|
||||||
|
<g ref="hoverLayer" :clip-path="`url(#${clipId})`" class="pointer-events-none" opacity="0">
|
||||||
|
<line
|
||||||
|
ref="hoverLine"
|
||||||
|
stroke="rgba(211, 227, 253, 0.42)"
|
||||||
|
stroke-dasharray="4 4"
|
||||||
|
stroke-width="1"
|
||||||
|
></line>
|
||||||
|
<circle ref="hoverDot" fill="#111418" r="0" stroke="#40dba2" stroke-width="2"></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="active && !points.length"
|
v-if="active && !points.length"
|
||||||
class="absolute inset-0 grid place-items-center text-[#8f949d]"
|
class="absolute inset-0 grid place-items-center text-[#8f949d]"
|
||||||
>
|
>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute right-2 top-2 flex items-center gap-1 rounded-md border border-[#2d3440] bg-[#111418]/90 px-1.5 py-1 shadow-sm"
|
v-if="notice"
|
||||||
|
class="pointer-events-none absolute left-3 top-3 max-w-[min(22rem,calc(100%-1.5rem))] rounded-md border border-[#3a424f] bg-[#111418]/90 px-3 py-2 text-xs text-[#d3e3fd] shadow-lg shadow-black/20"
|
||||||
>
|
>
|
||||||
<button
|
<div class="font-black text-[#ffcf6e]">{{ notice.title }}</div>
|
||||||
class="grid h-6 w-6 place-items-center rounded text-[#d3e3fd] hover:bg-[#1d232b]"
|
<div v-if="notice.detail" class="mt-0.5 text-[#8f949d]">{{ notice.detail }}</div>
|
||||||
type="button"
|
</div>
|
||||||
aria-label="縮小"
|
|
||||||
@click="zoomOut"
|
<div
|
||||||
|
class="pointer-events-none absolute w-[152px] translate-y-1 rounded-md border border-[#2d3440] bg-[#111418]/95 px-3 py-2 text-xs text-[#d3e3fd] opacity-0 shadow-lg shadow-black/30 transition-all duration-150 ease-out"
|
||||||
|
:class="tooltip.visible && 'translate-y-0 opacity-100'"
|
||||||
|
:style="tooltipStyle"
|
||||||
>
|
>
|
||||||
<Minus class="h-3.5 w-3.5" aria-hidden="true" />
|
<div class="font-black text-[#40dba2]">{{ tooltip.price }}</div>
|
||||||
</button>
|
<div class="mt-1 text-[#8f949d]">{{ tooltip.date }}</div>
|
||||||
<input
|
<div v-if="tooltip.detail" class="mt-1 leading-relaxed text-[#8f949d]">
|
||||||
:value="zoomScale"
|
{{ tooltip.detail }}
|
||||||
class="h-6 w-24 accent-[#40dba2]"
|
</div>
|
||||||
type="range"
|
|
||||||
:min="minZoom"
|
|
||||||
:max="maxZoom"
|
|
||||||
:step="zoomStep"
|
|
||||||
aria-label="グラフ縮尺"
|
|
||||||
@input="setZoomScaleFromInput"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="grid h-6 w-6 place-items-center rounded text-[#d3e3fd] hover:bg-[#1d232b]"
|
|
||||||
type="button"
|
|
||||||
aria-label="拡大"
|
|
||||||
@click="zoomIn"
|
|
||||||
>
|
|
||||||
<Plus class="h-3.5 w-3.5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="grid h-6 w-6 place-items-center rounded text-[#8f949d] hover:bg-[#1d232b] hover:text-[#d3e3fd]"
|
|
||||||
type="button"
|
|
||||||
aria-label="縮尺を戻す"
|
|
||||||
@click="resetZoom"
|
|
||||||
>
|
|
||||||
<RotateCcw class="h-3.5 w-3.5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDown, Download, FileCheck2, Search, ShieldCheck } from 'lucide-vue-next'
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
Download,
|
||||||
|
FileCheck2,
|
||||||
|
Minus,
|
||||||
|
Plus,
|
||||||
|
RotateCcw,
|
||||||
|
Search,
|
||||||
|
ShieldCheck,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
import { AnimatePresence, LayoutGroup, motion } from 'motion-v'
|
import { AnimatePresence, LayoutGroup, motion } from 'motion-v'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import Spinner from '../../components/ui/Spinner.vue'
|
import Spinner from '../../components/ui/Spinner.vue'
|
||||||
@@ -7,19 +16,17 @@ import UiButton from '../../components/ui/UiButton.vue'
|
|||||||
import UiField from '../../components/ui/UiField.vue'
|
import UiField from '../../components/ui/UiField.vue'
|
||||||
import UiSegmented from '../../components/ui/UiSegmented.vue'
|
import UiSegmented from '../../components/ui/UiSegmented.vue'
|
||||||
import {
|
import {
|
||||||
cashOrderAccountTypeOptions,
|
|
||||||
cashOrderMarketOptions,
|
|
||||||
cashOrderMethodOptions,
|
cashOrderMethodOptions,
|
||||||
cashOrderPriceConditionOptions,
|
cashOrderPriceConditionOptions,
|
||||||
cashOrderTermOptions,
|
|
||||||
cashOrderTriggerZoneOptions,
|
cashOrderTriggerZoneOptions,
|
||||||
orderKindOptions,
|
orderKindOptions,
|
||||||
sKabuOrderMarketOptions,
|
|
||||||
tradeSideOptions,
|
tradeSideOptions,
|
||||||
} from '../../constants/trade'
|
} from '../../constants/trade'
|
||||||
import { ui } from '../../styles/ui'
|
import { ui } from '../../styles/ui'
|
||||||
import type {
|
import type {
|
||||||
ChartMode,
|
ChartMode,
|
||||||
|
ChartNotice,
|
||||||
|
ChartRange,
|
||||||
CashOrderAccountType,
|
CashOrderAccountType,
|
||||||
CashOrderMarket,
|
CashOrderMarket,
|
||||||
CashOrderMethod,
|
CashOrderMethod,
|
||||||
@@ -36,6 +43,24 @@ import { currency } from '../../utils/format'
|
|||||||
import RealtimePriceChart from './RealtimePriceChart.vue'
|
import RealtimePriceChart from './RealtimePriceChart.vue'
|
||||||
|
|
||||||
type StockInfoTab = 'detail' | 'holding'
|
type StockInfoTab = 'detail' | 'holding'
|
||||||
|
type CashOrderAccountTypeOption = {
|
||||||
|
label: string
|
||||||
|
value: CashOrderAccountType
|
||||||
|
}
|
||||||
|
type CashOrderMarketOption = {
|
||||||
|
label: string
|
||||||
|
value: CashOrderMarket
|
||||||
|
}
|
||||||
|
type CashOrderTermOption = {
|
||||||
|
label: string
|
||||||
|
value: CashOrderTerm
|
||||||
|
}
|
||||||
|
type CashOrderDateOption = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartRangeOptions: ChartRange[] = ['1D', '7D', '1M', '3M', '1Y', 'ALL']
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
viewedStocks: Stock[]
|
viewedStocks: Stock[]
|
||||||
@@ -44,17 +69,16 @@ defineProps<{
|
|||||||
connected: boolean
|
connected: boolean
|
||||||
orderQuantity: number
|
orderQuantity: number
|
||||||
estimatedAmount: number
|
estimatedAmount: number
|
||||||
|
cashOrderAccountTypeOptions: CashOrderAccountTypeOption[]
|
||||||
|
cashOrderMarketOptions: CashOrderMarketOption[]
|
||||||
|
cashOrderTermOptions: CashOrderTermOption[]
|
||||||
|
cashOrderDateOptions: CashOrderDateOption[]
|
||||||
|
cashOrderPriceStep: number
|
||||||
canRequestCashEstimate: boolean
|
canRequestCashEstimate: boolean
|
||||||
canPlaceCashOrder: boolean
|
canPlaceCashOrder: boolean
|
||||||
realtimePricePoints: RealtimePricePoint[]
|
realtimePricePoints: RealtimePricePoint[]
|
||||||
|
chartNotice: ChartNotice | null
|
||||||
pricePolling: boolean
|
pricePolling: boolean
|
||||||
boxPlotStyle: {
|
|
||||||
min: string
|
|
||||||
q1: string
|
|
||||||
median: string
|
|
||||||
q3: string
|
|
||||||
max: string
|
|
||||||
}
|
|
||||||
hasQuote: (stock: Stock) => boolean
|
hasQuote: (stock: Stock) => boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -90,11 +114,10 @@ const cashOrderSecondaryPriceInput = defineModel<string>('cashOrderSecondaryPric
|
|||||||
const quantityInput = defineModel<string>('quantityInput', { required: true })
|
const quantityInput = defineModel<string>('quantityInput', { required: true })
|
||||||
const priceInput = defineModel<string>('priceInput', { required: true })
|
const priceInput = defineModel<string>('priceInput', { required: true })
|
||||||
const chartMode = defineModel<ChartMode>('chartMode', { required: true })
|
const chartMode = defineModel<ChartMode>('chartMode', { required: true })
|
||||||
|
const chartRange = defineModel<ChartRange>('chartRange', { required: true })
|
||||||
const activeStockInfoTab = ref<StockInfoTab>('detail')
|
const activeStockInfoTab = ref<StockInfoTab>('detail')
|
||||||
const advancedOptionsOpen = ref(false)
|
const advancedOptionsOpen = ref(false)
|
||||||
const orderMarketOptions = computed(() =>
|
const priceChart = ref<InstanceType<typeof RealtimePriceChart> | null>(null)
|
||||||
orderKind.value === 's' ? sKabuOrderMarketOptions : cashOrderMarketOptions,
|
|
||||||
)
|
|
||||||
const primaryPriceConditionRequiresPrice = computed(() =>
|
const primaryPriceConditionRequiresPrice = computed(() =>
|
||||||
['limit', 'limitAtOpen', 'limitAtClose', 'limitIoc', 'funari'].includes(
|
['limit', 'limitAtOpen', 'limitAtClose', 'limitIoc', 'funari'].includes(
|
||||||
cashOrderPriceCondition.value,
|
cashOrderPriceCondition.value,
|
||||||
@@ -178,12 +201,20 @@ const emit = defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="ui.periodTabs">
|
<div :class="ui.periodTabs">
|
||||||
<button :class="[ui.periodButton, ui.periodButtonActive]" type="button">1D</button>
|
<button
|
||||||
<button :class="ui.periodButton" type="button">1W</button>
|
v-for="range in chartRangeOptions"
|
||||||
<button :class="ui.periodButton" type="button">1M</button>
|
:key="range"
|
||||||
|
:class="[ui.periodButton, chartRange === range && ui.periodButtonActive]"
|
||||||
|
type="button"
|
||||||
|
:aria-pressed="chartRange === range"
|
||||||
|
@click="chartRange = range"
|
||||||
|
>
|
||||||
|
{{ range }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="ui.chartActions">
|
<div :class="ui.chartActions">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<div :class="ui.smallTabs">
|
<div :class="ui.smallTabs">
|
||||||
<button
|
<button
|
||||||
:class="[ui.smallTab, chartMode === 'line' && ui.smallTabActive]"
|
:class="[ui.smallTab, chartMode === 'line' && ui.smallTabActive]"
|
||||||
@@ -200,59 +231,58 @@ const emit = defineEmits<{
|
|||||||
箱ひげ
|
箱ひげ
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
class="grid h-8 w-8 place-items-center rounded-md border border-[#2d3440] text-[#d3e3fd] transition hover:bg-[#1d232b] disabled:opacity-40"
|
||||||
|
type="button"
|
||||||
|
aria-label="縮小"
|
||||||
|
:disabled="!hasQuote(selectedStock)"
|
||||||
|
@click="priceChart?.zoomOut()"
|
||||||
|
>
|
||||||
|
<Minus class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="grid h-8 w-8 place-items-center rounded-md border border-[#2d3440] text-[#d3e3fd] transition hover:bg-[#1d232b] disabled:opacity-40"
|
||||||
|
type="button"
|
||||||
|
aria-label="拡大"
|
||||||
|
:disabled="!hasQuote(selectedStock)"
|
||||||
|
@click="priceChart?.zoomIn()"
|
||||||
|
>
|
||||||
|
<Plus class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="grid h-8 w-8 place-items-center rounded-md border border-[#2d3440] text-[#8f949d] transition hover:bg-[#1d232b] hover:text-[#d3e3fd] disabled:opacity-40"
|
||||||
|
type="button"
|
||||||
|
aria-label="縮尺を戻す"
|
||||||
|
:disabled="!hasQuote(selectedStock)"
|
||||||
|
@click="priceChart?.resetZoom()"
|
||||||
|
>
|
||||||
|
<RotateCcw class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button :class="ui.ghostButton" type="button" @click="emit('downloadCsv')">
|
<button :class="ui.ghostButton" type="button" @click="emit('downloadCsv')">
|
||||||
<Download class="h-4 w-4" aria-hidden="true" />
|
<Download class="h-4 w-4" aria-hidden="true" />
|
||||||
CSV
|
CSV
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="chartMode === 'line'" :class="ui.chartBox">
|
<div :class="ui.chartBox">
|
||||||
<RealtimePriceChart
|
<RealtimePriceChart
|
||||||
v-if="hasQuote(selectedStock)"
|
v-if="hasQuote(selectedStock)"
|
||||||
|
ref="priceChart"
|
||||||
:points="realtimePricePoints"
|
:points="realtimePricePoints"
|
||||||
:stock-name="selectedStock.name"
|
:stock-name="selectedStock.name"
|
||||||
:active="pricePolling"
|
:active="pricePolling"
|
||||||
|
:mode="chartMode"
|
||||||
|
:range="chartRange"
|
||||||
|
:previous-close="selectedStock.prevClose"
|
||||||
|
:notice="chartNotice"
|
||||||
/>
|
/>
|
||||||
<span v-else class="grid h-full place-items-center text-[#8f949d]">
|
<span v-else class="grid h-full place-items-center text-[#8f949d]">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="ui.boxplot">
|
|
||||||
<div :class="ui.boxplotScale">
|
|
||||||
<span>
|
|
||||||
<template v-if="hasQuote(selectedStock)">{{
|
|
||||||
currency(selectedStock.box.min)
|
|
||||||
}}</template>
|
|
||||||
<Spinner v-else size="sm" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<template v-if="hasQuote(selectedStock)">{{
|
|
||||||
currency(selectedStock.box.max)
|
|
||||||
}}</template>
|
|
||||||
<Spinner v-else size="sm" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="hasQuote(selectedStock)" :class="ui.boxplotTrack">
|
|
||||||
<i
|
|
||||||
:class="ui.whisker"
|
|
||||||
:style="{
|
|
||||||
left: boxPlotStyle.min,
|
|
||||||
width: `calc(${boxPlotStyle.max} - ${boxPlotStyle.min})`,
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
:class="ui.box"
|
|
||||||
:style="{
|
|
||||||
left: boxPlotStyle.q1,
|
|
||||||
width: `calc(${boxPlotStyle.q3} - ${boxPlotStyle.q1})`,
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
<i :class="ui.median" :style="{ left: boxPlotStyle.median }"></i>
|
|
||||||
</div>
|
|
||||||
<div v-else class="grid min-h-20 place-items-center text-[#8f949d]">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<LayoutGroup id="stock-info-tabs">
|
<LayoutGroup id="stock-info-tabs">
|
||||||
@@ -413,10 +443,30 @@ const emit = defineEmits<{
|
|||||||
:transition="advancedTransition"
|
:transition="advancedTransition"
|
||||||
>
|
>
|
||||||
<span :class="ui.advancedLabel">預り区分</span>
|
<span :class="ui.advancedLabel">預り区分</span>
|
||||||
<UiSegmented
|
<LayoutGroup id="cash-order-account-type">
|
||||||
v-model="cashOrderAccountType"
|
<div :class="ui.accountTypeGrid" role="radiogroup" aria-label="預り区分">
|
||||||
:options="cashOrderAccountTypeOptions"
|
<button
|
||||||
|
v-for="option in cashOrderAccountTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
:aria-checked="cashOrderAccountType === option.value"
|
||||||
|
:class="[
|
||||||
|
ui.accountTypeButton,
|
||||||
|
cashOrderAccountType === option.value && ui.accountTypeButtonActive,
|
||||||
|
]"
|
||||||
|
@click="cashOrderAccountType = option.value"
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
v-if="cashOrderAccountType === option.value"
|
||||||
|
layout-id="cash-order-account-type-indicator"
|
||||||
|
:class="ui.accountTypeIndicator"
|
||||||
|
:transition="tabTransition"
|
||||||
/>
|
/>
|
||||||
|
<span :class="ui.accountTypeText">{{ option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</LayoutGroup>
|
||||||
<UiField
|
<UiField
|
||||||
v-model="cashOrderMarket"
|
v-model="cashOrderMarket"
|
||||||
as="select"
|
as="select"
|
||||||
@@ -424,7 +474,7 @@ const emit = defineEmits<{
|
|||||||
:disabled="orderKind === 's'"
|
:disabled="orderKind === 's'"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in orderMarketOptions"
|
v-for="option in cashOrderMarketOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
@@ -447,6 +497,7 @@ const emit = defineEmits<{
|
|||||||
label="注文価格"
|
label="注文価格"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
:step="cashOrderPriceStep"
|
||||||
placeholder="価格を入力"
|
placeholder="価格を入力"
|
||||||
/>
|
/>
|
||||||
<UiField v-model="cashOrderTerm" as="select" label="有効期限">
|
<UiField v-model="cashOrderTerm" as="select" label="有効期限">
|
||||||
@@ -459,7 +510,21 @@ const emit = defineEmits<{
|
|||||||
</option>
|
</option>
|
||||||
</UiField>
|
</UiField>
|
||||||
<UiField
|
<UiField
|
||||||
v-if="cashOrderTerm === 'date'"
|
v-if="cashOrderTerm === 'date' && cashOrderDateOptions.length"
|
||||||
|
v-model="cashOrderDateInput"
|
||||||
|
as="select"
|
||||||
|
label="指定日"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in cashOrderDateOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</UiField>
|
||||||
|
<UiField
|
||||||
|
v-else-if="cashOrderTerm === 'date'"
|
||||||
v-model="cashOrderDateInput"
|
v-model="cashOrderDateInput"
|
||||||
label="指定日"
|
label="指定日"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -488,6 +553,7 @@ const emit = defineEmits<{
|
|||||||
label="逆指値価格"
|
label="逆指値価格"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
:step="cashOrderPriceStep"
|
||||||
placeholder="価格を入力"
|
placeholder="価格を入力"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -511,6 +577,7 @@ const emit = defineEmits<{
|
|||||||
label="OCO価格"
|
label="OCO価格"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
:step="cashOrderPriceStep"
|
||||||
placeholder="価格を入力"
|
placeholder="価格を入力"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
ChartNotice,
|
||||||
OrderPreview,
|
OrderPreview,
|
||||||
OrderRow,
|
OrderRow,
|
||||||
Position,
|
Position,
|
||||||
@@ -180,7 +181,83 @@ export const stockFromBoard = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartDateTimeToIso = (value: string) => {
|
const zonedPartsFormatter = (timeZone: string) =>
|
||||||
|
new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone,
|
||||||
|
hourCycle: 'h23',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
const zonedParts = (date: Date, timeZone: string) => {
|
||||||
|
const parts = Object.fromEntries(
|
||||||
|
zonedPartsFormatter(timeZone)
|
||||||
|
.formatToParts(date)
|
||||||
|
.filter((part) => part.type !== 'literal')
|
||||||
|
.map((part) => [part.type, Number(part.value)]),
|
||||||
|
) as Partial<Record<'year' | 'month' | 'day' | 'hour' | 'minute' | 'second', number>>
|
||||||
|
|
||||||
|
const missing = (['year', 'month', 'day', 'hour', 'minute', 'second'] as const).filter(
|
||||||
|
(part) => parts[part] == null,
|
||||||
|
)
|
||||||
|
if (missing.length) {
|
||||||
|
throw new Error(`Failed to read ${missing.join(', ')} from ${timeZone} date parts`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: parts.year as number,
|
||||||
|
month: parts.month as number,
|
||||||
|
day: parts.day as number,
|
||||||
|
hour: parts.hour as number,
|
||||||
|
minute: parts.minute as number,
|
||||||
|
second: parts.second as number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeZoneOffsetMs = (date: Date, timeZone: string) => {
|
||||||
|
const parts = zonedParts(date, timeZone)
|
||||||
|
return (
|
||||||
|
Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second) -
|
||||||
|
date.getTime()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const zonedDateTimeToUtcMs = (
|
||||||
|
timeZone: string,
|
||||||
|
parts: {
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
day: number
|
||||||
|
hour?: number
|
||||||
|
minute?: number
|
||||||
|
second?: number
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const utcGuess = Date.UTC(
|
||||||
|
parts.year,
|
||||||
|
parts.month - 1,
|
||||||
|
parts.day,
|
||||||
|
parts.hour ?? 0,
|
||||||
|
parts.minute ?? 0,
|
||||||
|
parts.second ?? 0,
|
||||||
|
)
|
||||||
|
return utcGuess - timeZoneOffsetMs(new Date(utcGuess), timeZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const startOfMarketDateUtcMs = (timeZone: string, now = new Date()) => {
|
||||||
|
const parts = zonedParts(now, timeZone)
|
||||||
|
return zonedDateTimeToUtcMs(timeZone, {
|
||||||
|
year: parts.year,
|
||||||
|
month: parts.month,
|
||||||
|
day: parts.day,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartDateTimeToIso = (value: string, timeZone = 'Asia/Tokyo') => {
|
||||||
const digits = value.replace(/\D/g, '')
|
const digits = value.replace(/\D/g, '')
|
||||||
if (digits.length < 8) return ''
|
if (digits.length < 8) return ''
|
||||||
const year = Number(digits.slice(0, 4))
|
const year = Number(digits.slice(0, 4))
|
||||||
@@ -188,19 +265,72 @@ const chartDateTimeToIso = (value: string) => {
|
|||||||
const day = Number(digits.slice(6, 8))
|
const day = Number(digits.slice(6, 8))
|
||||||
const hour = Number(digits.slice(8, 10) || '0')
|
const hour = Number(digits.slice(8, 10) || '0')
|
||||||
const minute = Number(digits.slice(10, 12) || '0')
|
const minute = Number(digits.slice(10, 12) || '0')
|
||||||
const date = new Date(year, month - 1, day, hour, minute)
|
const time = zonedDateTimeToUtcMs(timeZone, { year, month, day, hour, minute })
|
||||||
return Number.isNaN(date.getTime()) ? '' : date.toISOString()
|
return Number.isNaN(time) ? '' : new Date(time).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pricePointsFromIssueChart = (value: unknown): RealtimePricePoint[] =>
|
export const pricePointsFromIssueChart = (
|
||||||
|
value: unknown,
|
||||||
|
timeZone = 'Asia/Tokyo',
|
||||||
|
): RealtimePricePoint[] =>
|
||||||
asArray(asRecord(value).prices)
|
asArray(asRecord(value).prices)
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const price = asRecord(entry)
|
const price = asRecord(entry)
|
||||||
const at = chartDateTimeToIso(textValue(price.dateTime))
|
const at = chartDateTimeToIso(textValue(price.dateTime), timeZone)
|
||||||
return { at, price: numberValue(price.close) }
|
const close = numberValue(price.close)
|
||||||
|
return {
|
||||||
|
at,
|
||||||
|
price: close,
|
||||||
|
open: numberValue(price.open),
|
||||||
|
high: numberValue(price.high),
|
||||||
|
low: numberValue(price.low),
|
||||||
|
close,
|
||||||
|
volume: numberValue(price.volume, 0),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.filter((point) => point.at && point.price > 0)
|
.filter((point) => point.at && point.price > 0)
|
||||||
|
|
||||||
|
const japanDateFormatter = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Tokyo',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartDateTimeToLocalDate = (value: string) => {
|
||||||
|
const digits = value.replace(/\D/g, '')
|
||||||
|
if (digits.length < 8) return ''
|
||||||
|
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const japanToday = () => japanDateFormatter.format(new Date())
|
||||||
|
|
||||||
|
const isWeekendInJapan = () => {
|
||||||
|
const weekday = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: 'Asia/Tokyo',
|
||||||
|
weekday: 'short',
|
||||||
|
}).format(new Date())
|
||||||
|
return weekday === 'Sat' || weekday === 'Sun'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chartNoticeFromIssueChart = (value: unknown): ChartNotice | null => {
|
||||||
|
const chart = asRecord(value)
|
||||||
|
const latestDate = chartDateTimeToLocalDate(textValue(chart.latestDateTime))
|
||||||
|
const today = japanToday()
|
||||||
|
if (!latestDate || latestDate >= today) return null
|
||||||
|
|
||||||
|
const formattedLatestDate = new Intl.DateTimeFormat('ja-JP', {
|
||||||
|
timeZone: 'Asia/Tokyo',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(new Date(`${latestDate}T00:00:00+09:00`))
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: isWeekendInJapan() ? '本日は休場日です' : '本日の取引データはまだありません',
|
||||||
|
detail: `最新のチャートは${formattedLatestDate}です`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const positionFromApi = (value: unknown): Position | null => {
|
export const positionFromApi = (value: unknown): Position | null => {
|
||||||
const item = asRecord(value)
|
const item = asRecord(value)
|
||||||
const issue = issueFrom(item.issue)
|
const issue = issueFrom(item.issue)
|
||||||
@@ -243,7 +373,7 @@ export const orderFromApi = (value: unknown): OrderRow | null => {
|
|||||||
const quantity =
|
const quantity =
|
||||||
status === '約定済'
|
status === '約定済'
|
||||||
? (executedQuantity ?? orderedQuantity ?? unexecutedQuantity)
|
? (executedQuantity ?? orderedQuantity ?? unexecutedQuantity)
|
||||||
: (orderedQuantity ?? unexecutedQuantity ?? executedQuantity)
|
: (unexecutedQuantity ?? orderedQuantity ?? executedQuantity)
|
||||||
const price =
|
const price =
|
||||||
status === '約定済'
|
status === '約定済'
|
||||||
? (nullableNumberValue(item.executedPrice) ?? nullableNumberValue(item.price))
|
? (nullableNumberValue(item.executedPrice) ?? nullableNumberValue(item.price))
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { computed, ref, watch, type Ref } from 'vue'
|
import { computed, ref, watch, type Ref } from 'vue'
|
||||||
import { createRpcSocket } from '../../api'
|
import { createRpcSocket } from '../../api'
|
||||||
|
import {
|
||||||
|
cashOrderAccountTypeOptions as defaultCashOrderAccountTypeOptions,
|
||||||
|
cashOrderMarketOptions as defaultCashOrderMarketOptions,
|
||||||
|
sKabuOrderMarketOptions,
|
||||||
|
} from '../../constants/trade'
|
||||||
import type {
|
import type {
|
||||||
ChartMode,
|
ChartMode,
|
||||||
|
ChartRange,
|
||||||
|
ChartNotice,
|
||||||
CashOrderAccountType,
|
CashOrderAccountType,
|
||||||
CashOrderMarket,
|
CashOrderMarket,
|
||||||
CashOrderMethod,
|
CashOrderMethod,
|
||||||
@@ -29,8 +36,10 @@ import {
|
|||||||
orderFromApi,
|
orderFromApi,
|
||||||
orderHistoryKey,
|
orderHistoryKey,
|
||||||
orderHistoryResultNotice,
|
orderHistoryResultNotice,
|
||||||
|
chartNoticeFromIssueChart,
|
||||||
pricePointsFromIssueChart,
|
pricePointsFromIssueChart,
|
||||||
positionFromApi,
|
positionFromApi,
|
||||||
|
startOfMarketDateUtcMs,
|
||||||
stockFromBoard,
|
stockFromBoard,
|
||||||
stockFromIssue,
|
stockFromIssue,
|
||||||
stockFromPosition,
|
stockFromPosition,
|
||||||
@@ -43,6 +52,26 @@ type RpcResolver = {
|
|||||||
reject: (reason: Error) => void
|
reject: (reason: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CashOrderTermOption = {
|
||||||
|
label: string
|
||||||
|
value: CashOrderTerm
|
||||||
|
}
|
||||||
|
|
||||||
|
type CashOrderAccountTypeOption = {
|
||||||
|
label: string
|
||||||
|
value: CashOrderAccountType
|
||||||
|
}
|
||||||
|
|
||||||
|
type CashOrderMarketOption = {
|
||||||
|
label: string
|
||||||
|
value: CashOrderMarket
|
||||||
|
}
|
||||||
|
|
||||||
|
type CashOrderDateOption = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
const priceBasedCashOrderConditions = new Set<CashOrderPriceCondition>([
|
const priceBasedCashOrderConditions = new Set<CashOrderPriceCondition>([
|
||||||
'limit',
|
'limit',
|
||||||
'limitAtOpen',
|
'limitAtOpen',
|
||||||
@@ -54,6 +83,77 @@ const priceBasedCashOrderConditions = new Set<CashOrderPriceCondition>([
|
|||||||
const cashOrderPriceConditionRequiresPrice = (condition: CashOrderPriceCondition) =>
|
const cashOrderPriceConditionRequiresPrice = (condition: CashOrderPriceCondition) =>
|
||||||
priceBasedCashOrderConditions.has(condition)
|
priceBasedCashOrderConditions.has(condition)
|
||||||
|
|
||||||
|
const chartRangeOptions = {
|
||||||
|
'1D': { period: 'minute', unit: 5, count: 120 },
|
||||||
|
'7D': { period: 'day', unit: 1, count: 7 },
|
||||||
|
'1M': { period: 'day', unit: 1, count: 31 },
|
||||||
|
'3M': { period: 'day', unit: 1, count: 93 },
|
||||||
|
'1Y': { period: 'day', unit: 1, count: 365 },
|
||||||
|
ALL: { period: 'month', unit: 1, count: 9999 },
|
||||||
|
} as const satisfies Record<
|
||||||
|
ChartRange,
|
||||||
|
{ period: 'minute' | 'day' | 'week' | 'month'; unit: number; count: number }
|
||||||
|
>
|
||||||
|
|
||||||
|
const marketTimeZones: Record<string, string> = {
|
||||||
|
FKO: 'Asia/Tokyo',
|
||||||
|
NGY: 'Asia/Tokyo',
|
||||||
|
PTS: 'Asia/Tokyo',
|
||||||
|
PTX: 'Asia/Tokyo',
|
||||||
|
SOR: 'Asia/Tokyo',
|
||||||
|
SPR: 'Asia/Tokyo',
|
||||||
|
STK: 'Asia/Tokyo',
|
||||||
|
TKY: 'Asia/Tokyo',
|
||||||
|
AMEX: 'America/New_York',
|
||||||
|
ARCX: 'America/New_York',
|
||||||
|
NAS: 'America/New_York',
|
||||||
|
NASDAQ: 'America/New_York',
|
||||||
|
NYS: 'America/New_York',
|
||||||
|
NYSE: 'America/New_York',
|
||||||
|
XNAS: 'America/New_York',
|
||||||
|
XNYS: 'America/New_York',
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryTimeZones: Record<string, string> = {
|
||||||
|
アメリカ: 'America/New_York',
|
||||||
|
日本: 'Asia/Tokyo',
|
||||||
|
米国: 'America/New_York',
|
||||||
|
US: 'America/New_York',
|
||||||
|
USA: 'America/New_York',
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeZoneForStock = (stock: Stock) =>
|
||||||
|
marketTimeZones[stock.market.toUpperCase()] ?? countryTimeZones[stock.country] ?? null
|
||||||
|
|
||||||
|
const normalizeApkOrderTermDate = (value: string) => {
|
||||||
|
const digits = value.replace(/\D/g, '')
|
||||||
|
if (digits.length === 6) return `20${digits}`
|
||||||
|
if (digits.length === 8) return digits
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayApkOrderTermDate = (value: string) => {
|
||||||
|
const normalized = normalizeApkOrderTermDate(value)
|
||||||
|
if (!normalized) return value
|
||||||
|
return `${normalized.slice(0, 4)}/${normalized.slice(4, 6)}/${normalized.slice(6, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const apkExchangeMarketCodes: CashOrderMarket[] = ['SOR', 'TKY', 'NGY', 'FKO', 'SPR', 'PTS', 'PTX']
|
||||||
|
|
||||||
|
const parseApkExchangeMarkets = (value: string) => {
|
||||||
|
const markets: CashOrderMarket[] = []
|
||||||
|
for (let index = 0; index < value.length; index += 3) {
|
||||||
|
const code = value.slice(index, index + 3) as CashOrderMarket
|
||||||
|
if (apkExchangeMarketCodes.includes(code) && !markets.includes(code)) markets.push(code)
|
||||||
|
}
|
||||||
|
return markets
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceMatchesStep = (price: number, step: number) => {
|
||||||
|
if (!Number.isFinite(price) || !Number.isFinite(step) || step <= 0) return true
|
||||||
|
return Math.abs(price / step - Math.round(price / step)) < 1e-8
|
||||||
|
}
|
||||||
|
|
||||||
export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
||||||
const selectedStockCode = ref('')
|
const selectedStockCode = ref('')
|
||||||
const viewedStockCodes = ref<string[]>([])
|
const viewedStockCodes = ref<string[]>([])
|
||||||
@@ -72,6 +172,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const quantityInput = ref('')
|
const quantityInput = ref('')
|
||||||
const priceInput = ref('')
|
const priceInput = ref('')
|
||||||
const chartMode = ref<ChartMode>('line')
|
const chartMode = ref<ChartMode>('line')
|
||||||
|
const chartRange = ref<ChartRange>('1D')
|
||||||
const showSearch = ref(false)
|
const showSearch = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const countryFilter = ref('all')
|
const countryFilter = ref('all')
|
||||||
@@ -81,6 +182,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const pendingCashEstimateId = ref<number | null>(null)
|
const pendingCashEstimateId = ref<number | null>(null)
|
||||||
const lastCashEstimate = ref<OrderPreview | null>(null)
|
const lastCashEstimate = ref<OrderPreview | null>(null)
|
||||||
const lastCashEstimateKey = ref('')
|
const lastCashEstimateKey = ref('')
|
||||||
|
const cashPreOrder = ref<RecordLike | null>(null)
|
||||||
const ws = ref<WebSocket | null>(null)
|
const ws = ref<WebSocket | null>(null)
|
||||||
const rpcPending = new Map<number, RpcResolver>()
|
const rpcPending = new Map<number, RpcResolver>()
|
||||||
const sbiConnected = ref(false)
|
const sbiConnected = ref(false)
|
||||||
@@ -96,16 +198,16 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const positions = ref<Position[]>([])
|
const positions = ref<Position[]>([])
|
||||||
const stocks = ref<Stock[]>([])
|
const stocks = ref<Stock[]>([])
|
||||||
const historicalPricePoints = ref<RealtimePricePoint[]>([])
|
const historicalPricePoints = ref<RealtimePricePoint[]>([])
|
||||||
|
const chartNotice = ref<ChartNotice | null>(null)
|
||||||
const realtimePricePoints = ref<RealtimePricePoint[]>([])
|
const realtimePricePoints = ref<RealtimePricePoint[]>([])
|
||||||
const pricePolling = ref(false)
|
const pricePolling = ref(false)
|
||||||
let rpcId = 0
|
let rpcId = 0
|
||||||
let boardPollingSubscriptionId = ''
|
let boardPollingSubscriptionId = ''
|
||||||
let boardPollingRequestId = 0
|
let boardPollingRequestId = 0
|
||||||
let chartHistoryRequestId = 0
|
let chartHistoryRequestId = 0
|
||||||
|
let cashPreOrderRequestId = 0
|
||||||
|
|
||||||
const maxRealtimePricePoints = 120
|
const maxRealtimePricePoints = 120
|
||||||
const maxChartPricePoints = 240
|
|
||||||
|
|
||||||
const errorMessage = (cause: unknown, fallback: string) =>
|
const errorMessage = (cause: unknown, fallback: string) =>
|
||||||
cause instanceof Error ? cause.message : fallback
|
cause instanceof Error ? cause.message : fallback
|
||||||
|
|
||||||
@@ -139,6 +241,111 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
if (orderKind.value === 's') return 'STK'
|
if (orderKind.value === 's') return 'STK'
|
||||||
return cashOrderMarket.value === 'auto' ? selectedStock.value.market : cashOrderMarket.value
|
return cashOrderMarket.value === 'auto' ? selectedStock.value.market : cashOrderMarket.value
|
||||||
})
|
})
|
||||||
|
const sKabuAvailable = computed(() => {
|
||||||
|
if (orderKind.value !== 's') return true
|
||||||
|
const sKabu = cashPreOrder.value ? asRecord(cashPreOrder.value.sKabu) : null
|
||||||
|
return sKabu?.available !== false
|
||||||
|
})
|
||||||
|
const cashOrderIppanMarginPaymentLimit = computed(() => {
|
||||||
|
const margin = cashPreOrder.value ? asRecord(cashPreOrder.value.margin) : {}
|
||||||
|
return textValue(margin.ippanPaymentLimit) || undefined
|
||||||
|
})
|
||||||
|
const cashOrderAccountTypeOptions = computed<CashOrderAccountTypeOption[]>(() => {
|
||||||
|
const nisa = cashPreOrder.value ? asRecord(cashPreOrder.value.nisa) : {}
|
||||||
|
const options = defaultCashOrderAccountTypeOptions.filter(
|
||||||
|
(option) => option.value === 'specific' || option.value === 'general',
|
||||||
|
)
|
||||||
|
if (textValue(nisa.growthServiceKbn) === '1') {
|
||||||
|
const growth = defaultCashOrderAccountTypeOptions.find(
|
||||||
|
(option) => option.value === 'growthInvestment',
|
||||||
|
)
|
||||||
|
if (growth) options.push(growth)
|
||||||
|
}
|
||||||
|
if (textValue(nisa.serviceKbn) === '1') {
|
||||||
|
const nisaOption = defaultCashOrderAccountTypeOptions.find(
|
||||||
|
(option) => option.value === 'nisa',
|
||||||
|
)
|
||||||
|
if (nisaOption) options.push(nisaOption)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
const cashOrderMarketOptions = computed<CashOrderMarketOption[]>(() => {
|
||||||
|
if (orderKind.value === 's') return sKabuOrderMarketOptions
|
||||||
|
const exchangeList = textValue(cashPreOrder.value?.exchangeList)
|
||||||
|
if (!exchangeList) return defaultCashOrderMarketOptions
|
||||||
|
const markets = parseApkExchangeMarkets(exchangeList)
|
||||||
|
const options = markets
|
||||||
|
.map((market) => defaultCashOrderMarketOptions.find((option) => option.value === market))
|
||||||
|
.filter((option): option is CashOrderMarketOption => Boolean(option))
|
||||||
|
return options.length ? options : defaultCashOrderMarketOptions
|
||||||
|
})
|
||||||
|
const preferredCashOrderMarket = computed(() => {
|
||||||
|
if (orderKind.value === 's') return 'STK'
|
||||||
|
const index = Number(textValue(cashPreOrder.value?.exchangeListIndexFlag))
|
||||||
|
const indexedOption =
|
||||||
|
Number.isInteger(index) && index > 0 ? cashOrderMarketOptions.value[index - 1] : undefined
|
||||||
|
return (
|
||||||
|
indexedOption?.value ??
|
||||||
|
cashOrderMarketOptions.value.find((option) => option.value === selectedStock.value.market)
|
||||||
|
?.value ??
|
||||||
|
cashOrderMarketOptions.value[0]?.value ??
|
||||||
|
'auto'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const apkOrderTerms = computed(() =>
|
||||||
|
asArray(cashPreOrder.value?.orderTerms)
|
||||||
|
.map((value) => textValue(value))
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
const apkOrderTermDates = computed(() =>
|
||||||
|
asArray(cashPreOrder.value?.orderTermDates)
|
||||||
|
.map((value) => textValue(value))
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
const cashOrderTermOptions = computed<CashOrderTermOption[]>(() => {
|
||||||
|
if (orderKind.value === 's') return [{ label: '当日中', value: 'day' }]
|
||||||
|
const terms = apkOrderTerms.value
|
||||||
|
if (!terms.length) {
|
||||||
|
return [
|
||||||
|
{ label: '当日中', value: 'day' },
|
||||||
|
{ label: '今週中', value: 'week' },
|
||||||
|
{ label: '日付指定', value: 'date' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const options: CashOrderTermOption[] = []
|
||||||
|
if (terms.some((term) => term === '当日中')) options.push({ label: '当日中', value: 'day' })
|
||||||
|
if (terms.some((term) => term === '今週中')) options.push({ label: '今週中', value: 'week' })
|
||||||
|
if (apkOrderTermDates.value.length || terms.some((term) => /\d/.test(term))) {
|
||||||
|
options.push({ label: '日付指定', value: 'date' })
|
||||||
|
}
|
||||||
|
return options.length ? options : [{ label: '当日中', value: 'day' }]
|
||||||
|
})
|
||||||
|
const cashOrderDateOptions = computed<CashOrderDateOption[]>(() => {
|
||||||
|
const dates = apkOrderTermDates.value.length
|
||||||
|
? apkOrderTermDates.value
|
||||||
|
: apkOrderTerms.value.filter((term) => /\d/.test(term))
|
||||||
|
return dates
|
||||||
|
.map((date) => ({
|
||||||
|
label: displayApkOrderTermDate(date),
|
||||||
|
value: normalizeApkOrderTermDate(date),
|
||||||
|
}))
|
||||||
|
.filter((option) => option.value)
|
||||||
|
})
|
||||||
|
const cashOrderPriceStep = computed(() => {
|
||||||
|
const steps = asArray(cashPreOrder.value?.priceSteps)
|
||||||
|
.map((value) => {
|
||||||
|
const record = asRecord(value)
|
||||||
|
return {
|
||||||
|
upper: numberValue(record.from),
|
||||||
|
step: numberValue(record.to),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((step) => step.upper > 0 && step.step > 0)
|
||||||
|
.sort((left, right) => left.upper - right.upper)
|
||||||
|
if (!steps.length) return 1
|
||||||
|
const referencePrice = orderPrice.value > 0 ? orderPrice.value : selectedStock.value.price
|
||||||
|
return steps.find((step) => referencePrice <= step.upper)?.step ?? steps.at(-1)?.step ?? 1
|
||||||
|
})
|
||||||
const estimatedAmount = computed(() => Math.max(0, orderQuantity.value * orderPrice.value))
|
const estimatedAmount = computed(() => Math.max(0, orderQuantity.value * orderPrice.value))
|
||||||
const hasQuote = (stock: Stock) => stock.price > 0
|
const hasQuote = (stock: Stock) => stock.price > 0
|
||||||
const hasAccountSummary = computed(
|
const hasAccountSummary = computed(
|
||||||
@@ -154,10 +361,12 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
issueCode: selectedStock.value.code,
|
issueCode: selectedStock.value.code,
|
||||||
market: resolvedCashOrderMarket.value,
|
market: resolvedCashOrderMarket.value,
|
||||||
|
preOrderMarket: orderKind.value === 's' ? selectedStock.value.market : undefined,
|
||||||
side: tradeSide.value,
|
side: tradeSide.value,
|
||||||
quantity: orderQuantity.value,
|
quantity: orderQuantity.value,
|
||||||
kind: orderKind.value,
|
kind: orderKind.value,
|
||||||
accountType: cashOrderAccountType.value,
|
accountType: cashOrderAccountType.value,
|
||||||
|
depositType: cashOrderAccountType.value,
|
||||||
priceCondition: orderKind.value === 's' ? undefined : cashOrderPriceCondition.value,
|
priceCondition: orderKind.value === 's' ? undefined : cashOrderPriceCondition.value,
|
||||||
price:
|
price:
|
||||||
orderKind.value !== 's' && cashOrderPrimaryRequiresPrice.value
|
orderKind.value !== 's' && cashOrderPrimaryRequiresPrice.value
|
||||||
@@ -187,14 +396,30 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
cashOrderSecondaryRequiresPrice.value
|
cashOrderSecondaryRequiresPrice.value
|
||||||
? cashOrderSecondaryPrice.value
|
? cashOrderSecondaryPrice.value
|
||||||
: undefined,
|
: undefined,
|
||||||
|
ippanMarginPaymentLimit:
|
||||||
|
orderKind.value !== 's' ? cashOrderIppanMarginPaymentLimit.value : undefined,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const canRequestCashEstimate = computed(() => {
|
const canRequestCashEstimate = computed(() => {
|
||||||
if (!connected.value || !selectedStock.value.code || orderQuantity.value <= 0) return false
|
if (!connected.value || !selectedStock.value.code || orderQuantity.value <= 0) return false
|
||||||
|
if (orderKind.value === 's' && !selectedStock.value.market) return false
|
||||||
|
if (!sKabuAvailable.value) return false
|
||||||
if (orderKind.value === 's') return true
|
if (orderKind.value === 's') return true
|
||||||
if (cashOrderPrimaryRequiresPrice.value && orderPrice.value <= 0) return false
|
if (cashOrderPrimaryRequiresPrice.value && orderPrice.value <= 0) return false
|
||||||
|
if (
|
||||||
|
cashOrderPrimaryRequiresPrice.value &&
|
||||||
|
!priceMatchesStep(orderPrice.value, cashOrderPriceStep.value)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (cashOrderTerm.value === 'date' && !cashOrderDateInput.value) return false
|
if (cashOrderTerm.value === 'date' && !cashOrderDateInput.value) return false
|
||||||
if (cashOrderMethod.value !== 'normal' && cashOrderTriggerPrice.value <= 0) return false
|
if (cashOrderMethod.value !== 'normal' && cashOrderTriggerPrice.value <= 0) return false
|
||||||
|
if (
|
||||||
|
cashOrderMethod.value !== 'normal' &&
|
||||||
|
!priceMatchesStep(cashOrderTriggerPrice.value, cashOrderPriceStep.value)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
cashOrderMethod.value === 'oco' &&
|
cashOrderMethod.value === 'oco' &&
|
||||||
cashOrderSecondaryRequiresPrice.value &&
|
cashOrderSecondaryRequiresPrice.value &&
|
||||||
@@ -202,6 +427,13 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
cashOrderMethod.value === 'oco' &&
|
||||||
|
cashOrderSecondaryRequiresPrice.value &&
|
||||||
|
!priceMatchesStep(cashOrderSecondaryPrice.value, cashOrderPriceStep.value)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
const canPlaceCashOrder = computed(
|
const canPlaceCashOrder = computed(
|
||||||
@@ -251,22 +483,20 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
if (!totalAssetValue.value) return 0
|
if (!totalAssetValue.value) return 0
|
||||||
return (buyingPower.value / totalAssetValue.value) * 100
|
return (buyingPower.value / totalAssetValue.value) * 100
|
||||||
})
|
})
|
||||||
const chartPricePoints = computed(() =>
|
const selectedStockTimeZone = computed(() => timeZoneForStock(selectedStock.value))
|
||||||
[...historicalPricePoints.value, ...realtimePricePoints.value].slice(-maxChartPricePoints),
|
const chartPricePoints = computed(() => {
|
||||||
)
|
const points = [...historicalPricePoints.value, ...realtimePricePoints.value]
|
||||||
const boxPlotStyle = computed(() => {
|
if (chartRange.value !== '1D') return points
|
||||||
const box = selectedStock.value.box
|
|
||||||
const range = Math.max(1, box.max - box.min)
|
|
||||||
const toPct = (value: number) => ((value - box.min) / range) * 100
|
|
||||||
return {
|
|
||||||
min: `${toPct(box.min)}%`,
|
|
||||||
q1: `${toPct(box.q1)}%`,
|
|
||||||
median: `${toPct(box.median)}%`,
|
|
||||||
q3: `${toPct(box.q3)}%`,
|
|
||||||
max: `${toPct(box.max)}%`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const timeZone = selectedStockTimeZone.value
|
||||||
|
if (!timeZone) return points
|
||||||
|
|
||||||
|
const start = startOfMarketDateUtcMs(timeZone)
|
||||||
|
return points.filter((point) => {
|
||||||
|
const time = Date.parse(point.at)
|
||||||
|
return Number.isFinite(time) && time >= start
|
||||||
|
})
|
||||||
|
})
|
||||||
const recordViewedStock = (code: string) => {
|
const recordViewedStock = (code: string) => {
|
||||||
if (!code) return
|
if (!code) return
|
||||||
viewedStockCodes.value = [code, ...viewedStockCodes.value.filter((entry) => entry !== code)]
|
viewedStockCodes.value = [code, ...viewedStockCodes.value.filter((entry) => entry !== code)]
|
||||||
@@ -289,7 +519,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
if (!Number.isFinite(price) || price <= 0) return
|
if (!Number.isFinite(price) || price <= 0) return
|
||||||
realtimePricePoints.value = [
|
realtimePricePoints.value = [
|
||||||
...realtimePricePoints.value,
|
...realtimePricePoints.value,
|
||||||
{ at: at.toISOString(), price },
|
{ at: at.toISOString(), price, open: price, high: price, low: price, close: price },
|
||||||
].slice(-maxRealtimePricePoints)
|
].slice(-maxRealtimePricePoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +780,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
boardPollingSubscriptionId = ''
|
boardPollingSubscriptionId = ''
|
||||||
chartHistoryRequestId += 1
|
chartHistoryRequestId += 1
|
||||||
historicalPricePoints.value = []
|
historicalPricePoints.value = []
|
||||||
|
chartNotice.value = null
|
||||||
realtimePricePoints.value = []
|
realtimePricePoints.value = []
|
||||||
pricePolling.value = false
|
pricePolling.value = false
|
||||||
sbiConnected.value = false
|
sbiConnected.value = false
|
||||||
@@ -591,19 +822,27 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
|
|
||||||
const loadSelectedStockChart = async () => {
|
const loadSelectedStockChart = async () => {
|
||||||
historicalPricePoints.value = []
|
historicalPricePoints.value = []
|
||||||
|
chartNotice.value = null
|
||||||
const stock = selectedStock.value
|
const stock = selectedStock.value
|
||||||
if (!connected.value || !stock.code) return
|
if (!connected.value || !stock.code) return
|
||||||
|
|
||||||
const requestId = ++chartHistoryRequestId
|
const requestId = ++chartHistoryRequestId
|
||||||
|
const chartOptions = chartRangeOptions[chartRange.value]
|
||||||
try {
|
try {
|
||||||
|
const timeZone = selectedStockTimeZone.value
|
||||||
|
if (!timeZone) {
|
||||||
|
throw new Error(`Unsupported market timezone for ${stock.market || stock.country}`)
|
||||||
|
}
|
||||||
const chart = await rpcCall<RecordLike>('market.issue.chart', {
|
const chart = await rpcCall<RecordLike>('market.issue.chart', {
|
||||||
issueCode: stock.code,
|
issueCode: stock.code,
|
||||||
market: stock.market || undefined,
|
market: stock.market || undefined,
|
||||||
period: 'day',
|
period: chartOptions.period,
|
||||||
count: 120,
|
unit: chartOptions.unit,
|
||||||
|
count: chartOptions.count,
|
||||||
})
|
})
|
||||||
if (requestId !== chartHistoryRequestId) return
|
if (requestId !== chartHistoryRequestId) return
|
||||||
historicalPricePoints.value = pricePointsFromIssueChart(chart)
|
historicalPricePoints.value = pricePointsFromIssueChart(chart, timeZone)
|
||||||
|
chartNotice.value = chartNoticeFromIssueChart(chart)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (requestId === chartHistoryRequestId) {
|
if (requestId === chartHistoryRequestId) {
|
||||||
reportDataError(errorMessage(cause, '価格履歴の取得に失敗しました'), cause)
|
reportDataError(errorMessage(cause, '価格履歴の取得に失敗しました'), cause)
|
||||||
@@ -649,10 +888,12 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const cashOrderParams = () => ({
|
const cashOrderParams = () => ({
|
||||||
issueCode: selectedStock.value.code,
|
issueCode: selectedStock.value.code,
|
||||||
market: resolvedCashOrderMarket.value || undefined,
|
market: resolvedCashOrderMarket.value || undefined,
|
||||||
|
preOrderMarket: orderKind.value === 's' ? selectedStock.value.market || undefined : undefined,
|
||||||
side: tradeSide.value,
|
side: tradeSide.value,
|
||||||
quantity: orderQuantity.value,
|
quantity: orderQuantity.value,
|
||||||
kind: orderKind.value === 's' ? 's' : undefined,
|
kind: orderKind.value === 's' ? 's' : undefined,
|
||||||
accountType: cashOrderAccountType.value,
|
accountType: cashOrderAccountType.value,
|
||||||
|
depositType: cashOrderAccountType.value,
|
||||||
price:
|
price:
|
||||||
orderKind.value !== 's' && cashOrderPrimaryRequiresPrice.value ? orderPrice.value : undefined,
|
orderKind.value !== 's' && cashOrderPrimaryRequiresPrice.value ? orderPrice.value : undefined,
|
||||||
priceCondition: orderKind.value !== 's' ? cashOrderPriceCondition.value : undefined,
|
priceCondition: orderKind.value !== 's' ? cashOrderPriceCondition.value : undefined,
|
||||||
@@ -680,8 +921,36 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
cashOrderSecondaryRequiresPrice.value
|
cashOrderSecondaryRequiresPrice.value
|
||||||
? cashOrderSecondaryPrice.value
|
? cashOrderSecondaryPrice.value
|
||||||
: undefined,
|
: undefined,
|
||||||
|
ippanMarginPaymentLimit:
|
||||||
|
orderKind.value !== 's' ? cashOrderIppanMarginPaymentLimit.value : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const refreshCashPreOrder = async () => {
|
||||||
|
const requestId = ++cashPreOrderRequestId
|
||||||
|
if (!connected.value || !selectedStock.value.code || !resolvedCashOrderMarket.value) {
|
||||||
|
cashPreOrder.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const preOrder = await rpcCall<RecordLike>('orders.cash.preOrder', {
|
||||||
|
issueCode: selectedStock.value.code,
|
||||||
|
market: resolvedCashOrderMarket.value,
|
||||||
|
preOrderMarket:
|
||||||
|
orderKind.value === 's' ? selectedStock.value.market || undefined : undefined,
|
||||||
|
side: tradeSide.value,
|
||||||
|
kind: orderKind.value === 's' ? 's' : undefined,
|
||||||
|
accountType: cashOrderAccountType.value,
|
||||||
|
depositType: cashOrderAccountType.value,
|
||||||
|
})
|
||||||
|
if (requestId === cashPreOrderRequestId) cashPreOrder.value = preOrder
|
||||||
|
} catch (cause) {
|
||||||
|
if (requestId === cashPreOrderRequestId) {
|
||||||
|
cashPreOrder.value = null
|
||||||
|
reportDataError(errorMessage(cause, '注文前情報の取得に失敗しました'), cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const askPlaceOrder = () => {
|
const askPlaceOrder = () => {
|
||||||
if (!canPlaceCashOrder.value) return
|
if (!canPlaceCashOrder.value) return
|
||||||
showEstimateDialog.value = false
|
showEstimateDialog.value = false
|
||||||
@@ -783,7 +1052,35 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(cashOrderTerm, (term) => {
|
watch(cashOrderTerm, (term) => {
|
||||||
if (term !== 'date') cashOrderDateInput.value = ''
|
if (term !== 'date') {
|
||||||
|
cashOrderDateInput.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const firstDate = cashOrderDateOptions.value[0]?.value
|
||||||
|
if (firstDate && !cashOrderDateInput.value) cashOrderDateInput.value = firstDate
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(cashOrderAccountTypeOptions, (options) => {
|
||||||
|
if (options.some((option) => option.value === cashOrderAccountType.value)) return
|
||||||
|
cashOrderAccountType.value = options[0]?.value ?? 'specific'
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(cashOrderMarketOptions, (options) => {
|
||||||
|
if (options.some((option) => option.value === cashOrderMarket.value)) return
|
||||||
|
cashOrderMarket.value = preferredCashOrderMarket.value
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(cashOrderTermOptions, (options) => {
|
||||||
|
if (options.some((option) => option.value === cashOrderTerm.value)) return
|
||||||
|
cashOrderTerm.value = options[0]?.value ?? 'day'
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(cashOrderDateOptions, (options) => {
|
||||||
|
if (cashOrderTerm.value !== 'date') return
|
||||||
|
const firstOption = options[0]
|
||||||
|
if (!firstOption) return
|
||||||
|
if (options.some((option) => option.value === cashOrderDateInput.value)) return
|
||||||
|
cashOrderDateInput.value = firstOption.value
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(cashOrderMethod, (method) => {
|
watch(cashOrderMethod, (method) => {
|
||||||
@@ -795,6 +1092,22 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
if (method === 'stop') cashOrderSecondaryPriceInput.value = ''
|
if (method === 'stop') cashOrderSecondaryPriceInput.value = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
connected,
|
||||||
|
() => selectedStock.value.code,
|
||||||
|
() => selectedStock.value.market,
|
||||||
|
resolvedCashOrderMarket,
|
||||||
|
tradeSide,
|
||||||
|
orderKind,
|
||||||
|
cashOrderAccountType,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
void refreshCashPreOrder()
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
watch(searchQuery, (query) => {
|
watch(searchQuery, (query) => {
|
||||||
clearTimeout(searchTimer)
|
clearTimeout(searchTimer)
|
||||||
const trimmed = query.trim()
|
const trimmed = query.trim()
|
||||||
@@ -818,7 +1131,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[connected, () => selectedStock.value.code, () => selectedStock.value.market],
|
[connected, () => selectedStock.value.code, () => selectedStock.value.market, chartRange],
|
||||||
() => {
|
() => {
|
||||||
void loadSelectedStockChart()
|
void loadSelectedStockChart()
|
||||||
void startBoardPolling()
|
void startBoardPolling()
|
||||||
@@ -843,6 +1156,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
quantityInput,
|
quantityInput,
|
||||||
priceInput,
|
priceInput,
|
||||||
chartMode,
|
chartMode,
|
||||||
|
chartRange,
|
||||||
showSearch,
|
showSearch,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
countryFilter,
|
countryFilter,
|
||||||
@@ -863,6 +1177,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
positions,
|
positions,
|
||||||
realtimePricePoints,
|
realtimePricePoints,
|
||||||
chartPricePoints,
|
chartPricePoints,
|
||||||
|
chartNotice,
|
||||||
pricePolling,
|
pricePolling,
|
||||||
selectedStock,
|
selectedStock,
|
||||||
orderQuantity,
|
orderQuantity,
|
||||||
@@ -870,6 +1185,11 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
cashOrderPrimaryRequiresPrice,
|
cashOrderPrimaryRequiresPrice,
|
||||||
cashOrderTriggerPrice,
|
cashOrderTriggerPrice,
|
||||||
cashOrderSecondaryPrice,
|
cashOrderSecondaryPrice,
|
||||||
|
cashOrderAccountTypeOptions,
|
||||||
|
cashOrderMarketOptions,
|
||||||
|
cashOrderTermOptions,
|
||||||
|
cashOrderDateOptions,
|
||||||
|
cashOrderPriceStep,
|
||||||
estimatedAmount,
|
estimatedAmount,
|
||||||
showPortfolioSpinner,
|
showPortfolioSpinner,
|
||||||
canRequestCashEstimate,
|
canRequestCashEstimate,
|
||||||
@@ -883,7 +1203,6 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
totalAssetValue,
|
totalAssetValue,
|
||||||
stockAssetRatio,
|
stockAssetRatio,
|
||||||
cashAssetRatio,
|
cashAssetRatio,
|
||||||
boxPlotStyle,
|
|
||||||
hasQuote,
|
hasQuote,
|
||||||
selectStock,
|
selectStock,
|
||||||
connect,
|
connect,
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ export const router = createRouter({
|
|||||||
{ path: '/portfolio', name: 'portfolio', component: RouteStub },
|
{ path: '/portfolio', name: 'portfolio', component: RouteStub },
|
||||||
{ path: '/trade', name: 'trade', component: RouteStub },
|
{ path: '/trade', name: 'trade', component: RouteStub },
|
||||||
{ path: '/history', name: 'history', component: RouteStub },
|
{ path: '/history', name: 'history', component: RouteStub },
|
||||||
{ path: '/api-keys', redirect: '/settings' },
|
{ path: '/api-keys', redirect: '/settings/api-keys' },
|
||||||
{ path: '/settings', name: 'settings', component: RouteStub },
|
{ path: '/settings', redirect: '/settings/api-keys' },
|
||||||
|
{ path: '/settings/:section(api-keys|passkeys)', name: 'settings', component: RouteStub },
|
||||||
{ path: '/oauth/authorize', name: 'oauthAuthorize', component: RouteStub },
|
{ path: '/oauth/authorize', name: 'oauthAuthorize', component: RouteStub },
|
||||||
{ path: '/:pathMatch(.*)*', redirect: '/portfolio' },
|
{ path: '/:pathMatch(.*)*', redirect: '/portfolio' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ui = {
|
|||||||
stockHoldingNote:
|
stockHoldingNote:
|
||||||
'inline-flex w-fit items-center rounded-full border border-[#2f343b] bg-[#111418] px-4 py-2 text-sm font-semibold text-[#c3c7cf]',
|
'inline-flex w-fit items-center rounded-full border border-[#2f343b] bg-[#111418] px-4 py-2 text-sm font-semibold text-[#c3c7cf]',
|
||||||
quoteBox: 'grid justify-items-end gap-1',
|
quoteBox: 'grid justify-items-end gap-1',
|
||||||
periodTabs: 'flex justify-end gap-3',
|
periodTabs: 'flex flex-wrap justify-end gap-2',
|
||||||
periodButton: 'min-h-7 rounded-full bg-transparent px-3 text-xs font-bold text-[#8f949d]',
|
periodButton: 'min-h-7 rounded-full bg-transparent px-3 text-xs font-bold text-[#8f949d]',
|
||||||
periodButtonActive: 'bg-[#d3e3fd] text-[#102033]',
|
periodButtonActive: 'bg-[#d3e3fd] text-[#102033]',
|
||||||
chartActions: 'flex items-center justify-between gap-4',
|
chartActions: 'flex items-center justify-between gap-4',
|
||||||
@@ -120,6 +120,12 @@ export const ui = {
|
|||||||
advancedSummaryIcon: 'h-4 w-4',
|
advancedSummaryIcon: 'h-4 w-4',
|
||||||
advancedBody: 'grid gap-2 pt-3',
|
advancedBody: 'grid gap-2 pt-3',
|
||||||
advancedLabel: 'text-xs font-extrabold text-[#9aa0a9]',
|
advancedLabel: 'text-xs font-extrabold text-[#9aa0a9]',
|
||||||
|
accountTypeGrid: 'grid grid-cols-2 gap-2',
|
||||||
|
accountTypeButton:
|
||||||
|
'relative isolate grid min-h-11 place-items-center overflow-hidden rounded-[14px] bg-[#111418] px-2 py-2 text-center text-xs font-extrabold leading-snug text-[#9aa0a9] transition hover:bg-[#1d232b] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d3e3fd]',
|
||||||
|
accountTypeButtonActive: 'text-[#d3e3fd]',
|
||||||
|
accountTypeIndicator: 'absolute inset-0 -z-10 rounded-[14px] bg-[#263141]',
|
||||||
|
accountTypeText: 'relative block whitespace-normal break-keep leading-snug',
|
||||||
label: 'grid gap-2 text-xs font-extrabold text-[#9aa0a9]',
|
label: 'grid gap-2 text-xs font-extrabold text-[#9aa0a9]',
|
||||||
input:
|
input:
|
||||||
'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition placeholder:text-[#747982] focus:border-[#a8c7fa]',
|
'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition placeholder:text-[#747982] focus:border-[#a8c7fa]',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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'
|
export type CashOrderAccountType = 'specific' | 'general' | 'growthInvestment' | 'nisa'
|
||||||
export type CashOrderMarket = 'auto' | 'TKY' | 'SOR' | 'PTS' | 'PTX' | 'STK'
|
export type CashOrderMarket = 'auto' | 'TKY' | 'NGY' | 'FKO' | 'SPR' | 'SOR' | 'PTS' | 'PTX' | 'STK'
|
||||||
export type CashOrderPriceCondition =
|
export type CashOrderPriceCondition =
|
||||||
| 'limit'
|
| 'limit'
|
||||||
| 'limitAtOpen'
|
| 'limitAtOpen'
|
||||||
@@ -16,6 +16,7 @@ export type CashOrderTerm = 'day' | 'week' | 'date'
|
|||||||
export type CashOrderMethod = 'normal' | 'stop' | 'oco'
|
export type CashOrderMethod = 'normal' | 'stop' | 'oco'
|
||||||
export type CashOrderTriggerZone = 'above' | 'below'
|
export type CashOrderTriggerZone = 'above' | 'below'
|
||||||
export type ChartMode = 'line' | 'box'
|
export type ChartMode = 'line' | 'box'
|
||||||
|
export type ChartRange = '1D' | '7D' | '1M' | '3M' | '1Y' | 'ALL'
|
||||||
|
|
||||||
export type RpcMessage = {
|
export type RpcMessage = {
|
||||||
id: number
|
id: number
|
||||||
@@ -48,6 +49,16 @@ export type JsonRpcResponse = {
|
|||||||
export type RealtimePricePoint = {
|
export type RealtimePricePoint = {
|
||||||
at: string
|
at: string
|
||||||
price: number
|
price: number
|
||||||
|
open?: number
|
||||||
|
high?: number
|
||||||
|
low?: number
|
||||||
|
close?: number
|
||||||
|
volume?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartNotice = {
|
||||||
|
title: string
|
||||||
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Stock = {
|
export type Stock = {
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ COPY . .
|
|||||||
RUN bun --filter @repo/csbie build
|
RUN bun --filter @repo/csbie build
|
||||||
|
|
||||||
FROM oven/bun:1-slim AS runtime
|
FROM oven/bun:1-slim AS runtime
|
||||||
WORKDIR /app
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV CSBIE_DATABASE_PATH=/app/data/csbie.sqlite
|
||||||
|
WORKDIR /app
|
||||||
COPY --from=build /app/apps/csbie/dist ./apps/csbie/dist
|
COPY --from=build /app/apps/csbie/dist ./apps/csbie/dist
|
||||||
COPY --from=build /app/apps/csbie-ui/dist ./apps/csbie-ui/dist
|
COPY --from=build /app/apps/csbie-ui/dist ./apps/csbie-ui/dist
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY --from=build /app/node_modules/.bun/@napi-rs+keyring@*/node_modules/@napi-rs/keyring ./node_modules/@napi-rs/keyring
|
||||||
|
COPY --from=build /app/node_modules/.bun/@napi-rs+keyring-linux-x64-gnu@*/node_modules/@napi-rs/keyring-linux-x64-gnu ./node_modules/@napi-rs/keyring-linux-x64-gnu
|
||||||
|
WORKDIR /app/apps/csbie/dist
|
||||||
EXPOSE 8787
|
EXPOSE 8787
|
||||||
CMD ["bun", "apps/csbie/dist/index.js"]
|
CMD ["bun", "index.js"]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist ../csbie-ui/dist",
|
"clean": "rm -rf dist ../csbie-ui/dist",
|
||||||
"dev": "bun --env-file=../../.env src/dev.ts",
|
"dev": "bun --env-file=../../.env src/dev.ts",
|
||||||
"build": "bun --filter @repo/csbie-ui build && bun build src/index.ts --target=bun --outdir=dist",
|
"build": "bun --filter @repo/csbie-ui build && bun build src/index.ts --target=bun --outdir=dist --external @napi-rs/keyring",
|
||||||
"start": "bun --env-file=../../.env dist/index.js",
|
"start": "bun --env-file=../../.env dist/index.js",
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"name": "@repo/csbie-ui",
|
"name": "@repo/csbie-ui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.2",
|
"@simplewebauthn/browser": "^13.1.2",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"lucide-vue-next": "^1.0.0",
|
"lucide-vue-next": "^1.0.0",
|
||||||
"motion-v": "^2.3.0",
|
"motion-v": "^2.3.0",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vue-tsc": "^3",
|
"vue-tsc": "^3",
|
||||||
@@ -399,8 +401,72 @@
|
|||||||
|
|
||||||
"@types/chai": ["@types/[email protected]", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
"@types/chai": ["@types/[email protected]", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
"@types/d3": ["@types/[email protected]", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||||
|
|
||||||
|
"@types/d3-array": ["@types/[email protected]", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||||
|
|
||||||
|
"@types/d3-axis": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||||
|
|
||||||
|
"@types/d3-brush": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||||
|
|
||||||
|
"@types/d3-chord": ["@types/[email protected]", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||||
|
|
||||||
|
"@types/d3-color": ["@types/[email protected]", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||||
|
|
||||||
|
"@types/d3-contour": ["@types/[email protected]", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||||
|
|
||||||
|
"@types/d3-delaunay": ["@types/[email protected]", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||||
|
|
||||||
|
"@types/d3-dispatch": ["@types/[email protected]", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||||
|
|
||||||
|
"@types/d3-drag": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||||
|
|
||||||
|
"@types/d3-dsv": ["@types/[email protected]", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||||
|
|
||||||
|
"@types/d3-ease": ["@types/[email protected]", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||||
|
|
||||||
|
"@types/d3-fetch": ["@types/[email protected]", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||||
|
|
||||||
|
"@types/d3-force": ["@types/[email protected]", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||||
|
|
||||||
|
"@types/d3-format": ["@types/[email protected]", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||||
|
|
||||||
|
"@types/d3-geo": ["@types/[email protected]", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||||
|
|
||||||
|
"@types/d3-hierarchy": ["@types/[email protected]", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||||
|
|
||||||
|
"@types/d3-interpolate": ["@types/[email protected]", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||||
|
|
||||||
|
"@types/d3-path": ["@types/[email protected]", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||||
|
|
||||||
|
"@types/d3-polygon": ["@types/[email protected]", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||||
|
|
||||||
|
"@types/d3-quadtree": ["@types/[email protected]", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||||
|
|
||||||
|
"@types/d3-random": ["@types/[email protected]", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
|
||||||
|
|
||||||
|
"@types/d3-scale": ["@types/[email protected]", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||||
|
|
||||||
|
"@types/d3-scale-chromatic": ["@types/[email protected]", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||||
|
|
||||||
|
"@types/d3-selection": ["@types/[email protected]", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
|
||||||
|
|
||||||
|
"@types/d3-shape": ["@types/[email protected]", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||||
|
|
||||||
|
"@types/d3-time": ["@types/[email protected]", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||||
|
|
||||||
|
"@types/d3-time-format": ["@types/[email protected]", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||||
|
|
||||||
|
"@types/d3-timer": ["@types/[email protected]", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||||
|
|
||||||
|
"@types/d3-transition": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||||
|
|
||||||
|
"@types/d3-zoom": ["@types/[email protected]", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||||
|
|
||||||
"@types/deep-eql": ["@types/[email protected]", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
"@types/deep-eql": ["@types/[email protected]", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
|
"@types/geojson": ["@types/[email protected]", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||||
|
|
||||||
"@types/jsesc": ["@types/[email protected]", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
|
"@types/jsesc": ["@types/[email protected]", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
|
||||||
|
|
||||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
|
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
|
||||||
@@ -503,6 +569,8 @@
|
|||||||
|
|
||||||
"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
|
|
||||||
|
"commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
"confbox": ["[email protected]", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
"confbox": ["[email protected]", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||||
|
|
||||||
"content-disposition": ["[email protected]", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
"content-disposition": ["[email protected]", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||||
@@ -519,8 +587,72 @@
|
|||||||
|
|
||||||
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"d3": ["[email protected]", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||||
|
|
||||||
|
"d3-array": ["[email protected]", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||||
|
|
||||||
|
"d3-axis": ["[email protected]", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||||
|
|
||||||
|
"d3-brush": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||||
|
|
||||||
|
"d3-chord": ["[email protected]", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||||
|
|
||||||
|
"d3-color": ["[email protected]", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||||
|
|
||||||
|
"d3-contour": ["[email protected]", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||||
|
|
||||||
|
"d3-delaunay": ["[email protected]", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||||
|
|
||||||
|
"d3-dispatch": ["[email protected]", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||||
|
|
||||||
|
"d3-drag": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||||
|
|
||||||
|
"d3-dsv": ["[email protected]", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||||
|
|
||||||
|
"d3-ease": ["[email protected]", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||||
|
|
||||||
|
"d3-fetch": ["[email protected]", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||||
|
|
||||||
|
"d3-force": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||||
|
|
||||||
|
"d3-format": ["[email protected]", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||||
|
|
||||||
|
"d3-geo": ["[email protected]", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||||
|
|
||||||
|
"d3-hierarchy": ["[email protected]", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||||
|
|
||||||
|
"d3-interpolate": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||||
|
|
||||||
|
"d3-path": ["[email protected]", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||||
|
|
||||||
|
"d3-polygon": ["[email protected]", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||||
|
|
||||||
|
"d3-quadtree": ["[email protected]", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||||
|
|
||||||
|
"d3-random": ["[email protected]", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||||
|
|
||||||
|
"d3-scale": ["[email protected]", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||||
|
|
||||||
|
"d3-scale-chromatic": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||||
|
|
||||||
|
"d3-selection": ["[email protected]", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||||
|
|
||||||
|
"d3-shape": ["[email protected]", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||||
|
|
||||||
|
"d3-time": ["[email protected]", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||||
|
|
||||||
|
"d3-time-format": ["[email protected]", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||||
|
|
||||||
|
"d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||||
|
|
||||||
|
"d3-transition": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||||
|
|
||||||
|
"d3-zoom": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||||
|
|
||||||
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"delaunator": ["[email protected]", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
||||||
|
|
||||||
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
"detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
@@ -611,6 +743,8 @@
|
|||||||
|
|
||||||
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"internmap": ["[email protected]", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
||||||
"ip-address": ["[email protected]", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
"ip-address": ["[email protected]", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||||
|
|
||||||
"ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
@@ -755,10 +889,14 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"robust-predicates": ["[email protected]", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||||
|
|
||||||
"rolldown": ["[email protected]", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="],
|
"rolldown": ["[email protected]", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="],
|
||||||
|
|
||||||
"router": ["[email protected]", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
"router": ["[email protected]", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
|
"rw": ["[email protected]", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||||
|
|
||||||
"safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"scule": ["[email protected]", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
"scule": ["[email protected]", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
||||||
@@ -879,6 +1017,8 @@
|
|||||||
|
|
||||||
"body-parser/content-type": ["[email protected]", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
"body-parser/content-type": ["[email protected]", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||||
|
|
||||||
|
"d3-dsv/iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
"mlly/pkg-types": ["[email protected]", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"mlly/pkg-types": ["[email protected]", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
"tsx/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
"tsx/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
csbie:
|
||||||
|
image: git.yutakobayashi.com/nakasyou/csbi:latest
|
||||||
|
ports:
|
||||||
|
- '18787:8787'
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781577229,
|
||||||
|
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
description = "cSBI development and runtime helpers";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
systems = [
|
||||||
|
"aarch64-darwin"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"x86_64-linux"
|
||||||
|
];
|
||||||
|
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
bun
|
||||||
|
docker
|
||||||
|
docker-compose
|
||||||
|
git
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
apps = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
composeUp = pkgs.writeShellApplication {
|
||||||
|
name = "csbi-compose-up";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.docker-compose
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
exec docker-compose up "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${composeUp}/bin/csbi-compose-up";
|
||||||
|
meta.description = "Run cSBI with docker-compose";
|
||||||
|
};
|
||||||
|
|
||||||
|
compose-up = {
|
||||||
|
type = "app";
|
||||||
|
program = "${composeUp}/bin/csbi-compose-up";
|
||||||
|
meta.description = "Run cSBI with docker-compose";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
nixosModules.default =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.services.csbie;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.csbie = {
|
||||||
|
enable = lib.mkEnableOption "cSBI";
|
||||||
|
|
||||||
|
image = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "git.yutakobayashi.com/nakasyou/csbi:latest";
|
||||||
|
description = "Container image to run.";
|
||||||
|
};
|
||||||
|
|
||||||
|
envFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Environment file passed to the container.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/csbie";
|
||||||
|
description = "Host directory mounted at /app/data.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 18787;
|
||||||
|
description = "Host port exposed for cSBI.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
virtualisation.oci-containers.backend = lib.mkDefault "docker";
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.dataDir} 0750 root root -"
|
||||||
|
];
|
||||||
|
|
||||||
|
virtualisation.oci-containers.containers.csbie = {
|
||||||
|
image = cfg.image;
|
||||||
|
ports = [
|
||||||
|
"${toString cfg.port}:8787"
|
||||||
|
];
|
||||||
|
environmentFiles = [
|
||||||
|
cfg.envFile
|
||||||
|
];
|
||||||
|
volumes = [
|
||||||
|
"${cfg.dataDir}:/app/data"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -23,5 +23,12 @@ export type {
|
|||||||
IssueSearchItem,
|
IssueSearchItem,
|
||||||
IssueSearchResult,
|
IssueSearchResult,
|
||||||
IssueSearchStatus,
|
IssueSearchStatus,
|
||||||
|
OrderCorrectionPreOrder,
|
||||||
|
OrderCorrectionPreOrderDetail,
|
||||||
|
OrderPreview,
|
||||||
|
StockOrderPreOrder,
|
||||||
|
StockOrderPreOrderMarginTradeType,
|
||||||
|
StockOrderPreOrderPaymentLimit,
|
||||||
|
StockOrderPreOrderPriceStep,
|
||||||
} from './types'
|
} from './types'
|
||||||
export type * from './methods/types'
|
export type * from './methods/types'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import type {
|
|||||||
OrderKind,
|
OrderKind,
|
||||||
OrderList,
|
OrderList,
|
||||||
OrderPreview,
|
OrderPreview,
|
||||||
|
StockOrderPreOrder,
|
||||||
OrderReceipt,
|
OrderReceipt,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
PositionId,
|
PositionId,
|
||||||
@@ -156,6 +157,19 @@ export type CashOrderTriggerZone = 'above' | 'below'
|
|||||||
|
|
||||||
export type CashOrderMethod = 'normal' | 'stop' | 'oco'
|
export type CashOrderMethod = 'normal' | 'stop' | 'oco'
|
||||||
|
|
||||||
|
export type StockOrderMarginPosition = {
|
||||||
|
/** Open trade date from the margin position record, in yyyyMMdd or yyyy-MM-dd format. */
|
||||||
|
openTradeDate: string
|
||||||
|
/** Open price from the margin position record. Raw strings are accepted to preserve APK values. */
|
||||||
|
openPrice: number | string
|
||||||
|
/** Quantity selected from the margin position record. */
|
||||||
|
quantity: number | string
|
||||||
|
/** Original new-trade date from the margin position record, in yyyyMMdd or yyyy-MM-dd format. */
|
||||||
|
orgNewTradeDate: string
|
||||||
|
/** Bargain market code from the margin position record. */
|
||||||
|
bargainMarketCode: MarketCode
|
||||||
|
}
|
||||||
|
|
||||||
export type StandardCashOrderOptions = StockOrderBaseOptions & {
|
export type StandardCashOrderOptions = StockOrderBaseOptions & {
|
||||||
/** Order price for limit and other price-based orders. */
|
/** Order price for limit and other price-based orders. */
|
||||||
price?: number
|
price?: number
|
||||||
@@ -177,18 +191,68 @@ export type StandardCashOrderOptions = StockOrderBaseOptions & {
|
|||||||
secondaryPriceCondition?: CashOrderPriceCondition
|
secondaryPriceCondition?: CashOrderPriceCondition
|
||||||
/** Secondary order price used by OCO price-based conditions. */
|
/** Secondary order price used by OCO price-based conditions. */
|
||||||
secondaryPrice?: number
|
secondaryPrice?: number
|
||||||
|
/** APK ippan margin payment-limit code returned by stock board/pre-order information. */
|
||||||
|
ippanMarginPaymentLimit?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SKabuOrderOptions = StockOrderBaseOptions & {
|
export type SKabuOrderOptions = StockOrderBaseOptions & {
|
||||||
/** Places the cash order as an S-kabu order. S-kabu cannot specify a price. */
|
/** Places the cash order as an S-kabu order. S-kabu cannot specify a price. */
|
||||||
kind: 's'
|
kind: 's'
|
||||||
|
/** APK pre-order market for the underlying issue. The live S-kabu order still sends `market: "STK"`. */
|
||||||
|
preOrderMarket?: MarketCode
|
||||||
/** S-kabu cannot specify a price. */
|
/** S-kabu cannot specify a price. */
|
||||||
price?: never
|
price?: never
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CashOrderOptions = StandardCashOrderOptions | SKabuOrderOptions
|
export type CashOrderOptions = StandardCashOrderOptions | SKabuOrderOptions
|
||||||
|
|
||||||
export type MarginOpenOrderOptions = StandardCashOrderOptions
|
export type CashOrderPreOrderOptions = Pick<
|
||||||
|
StockOrderBaseOptions,
|
||||||
|
'issueCode' | 'market' | 'side' | 'accountType' | 'depositType'
|
||||||
|
> & {
|
||||||
|
/** Requests the APK S-kabu pre-order route constraints for this issue. */
|
||||||
|
kind?: 's'
|
||||||
|
/** APK pre-order market for S-kabu checks. The live S-kabu order still sends `market: "STK"`. */
|
||||||
|
preOrderMarket?: MarketCode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarginOpenTradeType =
|
||||||
|
| 'standard'
|
||||||
|
| 'generalBuy'
|
||||||
|
| 'generalSellShort'
|
||||||
|
| 'generalSellInventoryLimited'
|
||||||
|
| 'generalSellInventoryUnlimited'
|
||||||
|
| 'day'
|
||||||
|
| 'hyper'
|
||||||
|
|
||||||
|
export type MarginOpenOrderPreOrderOptions = Pick<
|
||||||
|
StockOrderBaseOptions,
|
||||||
|
'issueCode' | 'market' | 'side' | 'accountType' | 'depositType'
|
||||||
|
>
|
||||||
|
|
||||||
|
export type MarginOpenOrderOptions = StandardCashOrderOptions & {
|
||||||
|
/** APK margin-open trade type. Required because the mobile payload has no safe default. */
|
||||||
|
marginTradeType: MarginOpenTradeType
|
||||||
|
/** APK ippan margin payment-limit code, when returned by board/pre-order information. */
|
||||||
|
ippanMarginPaymentLimit?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarginCloseTradeType = 'sixMonth' | 'noLimit' | 'oneDay' | 'fifteenDay'
|
||||||
|
|
||||||
|
export type MarginCloseOrderPreOrderOptions = Pick<
|
||||||
|
StockOrderBaseOptions,
|
||||||
|
'issueCode' | 'market' | 'side' | 'accountType' | 'depositType'
|
||||||
|
> & {
|
||||||
|
/** APK margin-close trade type used by the mobile pre-order request. */
|
||||||
|
marginCloseTradeType?: MarginCloseTradeType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarginClosePositionOrder =
|
||||||
|
| 'profitFirst'
|
||||||
|
| 'lossFirst'
|
||||||
|
| 'newestFirst'
|
||||||
|
| 'oldestFirst'
|
||||||
|
| 'specify'
|
||||||
|
|
||||||
export type ActualDeliveryKind = 'genbiki' | 'genwatashi'
|
export type ActualDeliveryKind = 'genbiki' | 'genwatashi'
|
||||||
|
|
||||||
@@ -209,11 +273,22 @@ export type ActualDeliveryOrderOptions = {
|
|||||||
kind: ActualDeliveryKind
|
kind: ActualDeliveryKind
|
||||||
/** Position ID to deliver. */
|
/** Position ID to deliver. */
|
||||||
positionId?: PositionId
|
positionId?: PositionId
|
||||||
|
/** Margin position records selected for genbiki/genwatashi delivery. */
|
||||||
|
marginPositions?: StockOrderMarginPosition[]
|
||||||
|
/** APK ippan margin payment-limit code returned by stock board/pre-order information. */
|
||||||
|
ippanMarginPaymentLimit?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ActualDeliveryOrderPreOrderOptions = Pick<
|
||||||
|
ActualDeliveryOrderOptions,
|
||||||
|
'issueCode' | 'market' | 'accountType' | 'depositType' | 'kind'
|
||||||
|
>
|
||||||
|
|
||||||
export type PlaceCashOrderOptions = CashOrderOptions & {
|
export type PlaceCashOrderOptions = CashOrderOptions & {
|
||||||
/** Confirmation ID returned by the confirmation step. */
|
/** Confirmation ID returned by the confirmation step. */
|
||||||
confirmationId?: string
|
confirmationId?: string
|
||||||
|
/** APK confirmation-screen omission flag. Only valid for live submit calls. */
|
||||||
|
omitConfirmation?: boolean
|
||||||
/** Explicitly allows sending a live order. */
|
/** Explicitly allows sending a live order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
}
|
}
|
||||||
@@ -221,6 +296,8 @@ export type PlaceCashOrderOptions = CashOrderOptions & {
|
|||||||
export type PlaceMarginOpenOrderOptions = MarginOpenOrderOptions & {
|
export type PlaceMarginOpenOrderOptions = MarginOpenOrderOptions & {
|
||||||
/** Confirmation ID returned by the confirmation step. */
|
/** Confirmation ID returned by the confirmation step. */
|
||||||
confirmationId?: string
|
confirmationId?: string
|
||||||
|
/** APK confirmation-screen omission flag. Only valid for live submit calls. */
|
||||||
|
omitConfirmation?: boolean
|
||||||
/** Explicitly allows sending a live margin open order. */
|
/** Explicitly allows sending a live margin open order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
}
|
}
|
||||||
@@ -228,13 +305,59 @@ export type PlaceMarginOpenOrderOptions = MarginOpenOrderOptions & {
|
|||||||
export type PlaceActualDeliveryOrderOptions = ActualDeliveryOrderOptions & {
|
export type PlaceActualDeliveryOrderOptions = ActualDeliveryOrderOptions & {
|
||||||
/** Confirmation ID returned by the confirmation step. */
|
/** Confirmation ID returned by the confirmation step. */
|
||||||
confirmationId?: string
|
confirmationId?: string
|
||||||
|
/** APK confirmation-screen omission flag. Only valid for live submit calls. */
|
||||||
|
omitConfirmation?: boolean
|
||||||
/** Explicitly allows sending a live actual-delivery order. */
|
/** Explicitly allows sending a live actual-delivery order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderCorrectionOptions = {
|
export type OrderCorrectionOptions = {
|
||||||
|
/** Order number shown in order inquiry. Required by the mobile pre-correction route. */
|
||||||
|
orderNumber?: string
|
||||||
/** Order ID to correct. */
|
/** Order ID to correct. */
|
||||||
orderId: OrderId
|
orderId: OrderId
|
||||||
|
/** Issue code from the pre-correction response. */
|
||||||
|
issueCode?: IssueCode
|
||||||
|
/** Market code from the pre-correction response. */
|
||||||
|
market?: MarketCode
|
||||||
|
/** Original trade ID code. */
|
||||||
|
tradeId?: string
|
||||||
|
/** Additional correction flag used by the mobile MTS route. */
|
||||||
|
correctionType?: string
|
||||||
|
/** Original order status code from the pre-correction response. */
|
||||||
|
status?: string
|
||||||
|
/** Original RBE order status code from the pre-correction response. */
|
||||||
|
rbeOrderStatus?: string
|
||||||
|
/** Display deposit type text from the original order. */
|
||||||
|
depositTypeText?: string
|
||||||
|
/** Primary order method for correction. */
|
||||||
|
orderMethod?: CashOrderMethod
|
||||||
|
/** Corrected primary execution condition. */
|
||||||
|
priceCondition?: CashOrderPriceCondition
|
||||||
|
/** Stop trigger direction for correction. */
|
||||||
|
triggerZone?: CashOrderTriggerZone
|
||||||
|
/** Stop trigger price for correction. */
|
||||||
|
triggerPrice?: number
|
||||||
|
/** Secondary/OCO execution condition for correction. */
|
||||||
|
secondaryPriceCondition?: CashOrderPriceCondition
|
||||||
|
/** Secondary/OCO price for correction. */
|
||||||
|
secondaryPrice?: number
|
||||||
|
/** IFD follow-up execution condition for IF/IFDOCO correction. */
|
||||||
|
ifdPriceCondition?: CashOrderPriceCondition
|
||||||
|
/** IFD follow-up price for IF/IFDOCO correction. */
|
||||||
|
ifdPrice?: number
|
||||||
|
/** IFD follow-up special order method for correction. */
|
||||||
|
ifdOrderMethod?: CashOrderMethod
|
||||||
|
/** IFD follow-up stop trigger direction for correction. */
|
||||||
|
ifdTriggerZone?: CashOrderTriggerZone
|
||||||
|
/** IFD follow-up stop trigger price for correction. */
|
||||||
|
ifdTriggerPrice?: number
|
||||||
|
/** IFD follow-up secondary/OCO execution condition for correction. */
|
||||||
|
ifdSecondaryPriceCondition?: CashOrderPriceCondition
|
||||||
|
/** IFD follow-up secondary/OCO price for correction. */
|
||||||
|
ifdSecondaryPrice?: number
|
||||||
|
/** Mobile correction control flag. Defaults to normal mobile value when omitted. */
|
||||||
|
correctionControlFlag?: '1' | '2'
|
||||||
/** Corrected order quantity. */
|
/** Corrected order quantity. */
|
||||||
quantity?: number
|
quantity?: number
|
||||||
/** Corrected order price. */
|
/** Corrected order price. */
|
||||||
@@ -267,9 +390,17 @@ export type PlaceOrderCancelOptions = OrderCancelOptions & {
|
|||||||
export type MarginCloseOrderOptions = StandardCashOrderOptions & {
|
export type MarginCloseOrderOptions = StandardCashOrderOptions & {
|
||||||
/** Position ID to close. */
|
/** Position ID to close. */
|
||||||
positionId?: PositionId
|
positionId?: PositionId
|
||||||
|
/** APK margin-close trade type. Required because the mobile payload has no safe default. */
|
||||||
|
marginCloseTradeType: MarginCloseTradeType
|
||||||
|
/** Margin position records selected for specified close orders. */
|
||||||
|
marginPositions?: StockOrderMarginPosition[]
|
||||||
|
/** APK close-position ordering used by summary close orders. */
|
||||||
|
marginClosePositionOrder?: MarginClosePositionOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlaceMarginCloseOrderOptions = MarginCloseOrderOptions & {
|
export type PlaceMarginCloseOrderOptions = MarginCloseOrderOptions & {
|
||||||
|
/** APK confirmation-screen omission flag. Only valid for live submit calls. */
|
||||||
|
omitConfirmation?: boolean
|
||||||
/** Explicitly allows sending a live margin close order. */
|
/** Explicitly allows sending a live margin close order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
}
|
}
|
||||||
@@ -277,6 +408,8 @@ export type PlaceMarginCloseOrderOptions = MarginCloseOrderOptions & {
|
|||||||
export type MarginCloseSummaryOrderOptions = MarginCloseOrderOptions
|
export type MarginCloseSummaryOrderOptions = MarginCloseOrderOptions
|
||||||
|
|
||||||
export type PlaceMarginCloseSummaryOrderOptions = MarginCloseSummaryOrderOptions & {
|
export type PlaceMarginCloseSummaryOrderOptions = MarginCloseSummaryOrderOptions & {
|
||||||
|
/** APK confirmation-screen omission flag. Only valid for live submit calls. */
|
||||||
|
omitConfirmation?: boolean
|
||||||
/** Explicitly allows sending a live margin close summary order. */
|
/** Explicitly allows sending a live margin close summary order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
}
|
}
|
||||||
@@ -284,11 +417,35 @@ export type PlaceMarginCloseSummaryOrderOptions = MarginCloseSummaryOrderOptions
|
|||||||
export type IfdOrderOptions = StandardCashOrderOptions & {
|
export type IfdOrderOptions = StandardCashOrderOptions & {
|
||||||
/** Product to use for the first IFD leg. Defaults to cash. */
|
/** Product to use for the first IFD leg. Defaults to cash. */
|
||||||
tradeType?: 'cash' | 'marginOpen'
|
tradeType?: 'cash' | 'marginOpen'
|
||||||
|
/** APK margin-open trade type for the first leg when `tradeType` is `marginOpen`. */
|
||||||
|
marginTradeType?: MarginOpenTradeType
|
||||||
|
/** APK ippan margin payment-limit code for the first leg. */
|
||||||
|
ippanMarginPaymentLimit?: string
|
||||||
|
/** Execution condition for the IFD follow-up leg. */
|
||||||
|
ifdPriceCondition?: CashOrderPriceCondition
|
||||||
|
/** Order price for the IFD follow-up leg. */
|
||||||
|
ifdPrice?: number
|
||||||
|
/** Validity for the IFD follow-up leg. */
|
||||||
|
ifdOrderTerm?: CashOrderTerm
|
||||||
|
/** Explicit validity date for the IFD follow-up leg. */
|
||||||
|
ifdOrderDate?: string
|
||||||
|
/** Special order method for the IFD follow-up leg. */
|
||||||
|
ifdOrderMethod?: CashOrderMethod
|
||||||
|
/** Stop trigger direction for the IFD follow-up leg. */
|
||||||
|
ifdTriggerZone?: CashOrderTriggerZone
|
||||||
|
/** Stop trigger price for the IFD follow-up leg. */
|
||||||
|
ifdTriggerPrice?: number
|
||||||
|
/** Secondary OCO execution condition for IFDOCO. */
|
||||||
|
ifdSecondaryPriceCondition?: CashOrderPriceCondition
|
||||||
|
/** Secondary OCO price for IFDOCO. */
|
||||||
|
ifdSecondaryPrice?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlaceIfdOrderOptions = IfdOrderOptions & {
|
export type PlaceIfdOrderOptions = IfdOrderOptions & {
|
||||||
/** Confirmation ID returned by the confirmation step. */
|
/** Confirmation ID returned by the confirmation step. */
|
||||||
confirmationId?: string
|
confirmationId?: string
|
||||||
|
/** APK confirmation-screen omission flag. Only valid for live submit calls. */
|
||||||
|
omitConfirmation?: boolean
|
||||||
/** Explicitly allows sending a live IFD order. */
|
/** Explicitly allows sending a live IFD order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
}
|
}
|
||||||
@@ -296,12 +453,47 @@ export type PlaceIfdOrderOptions = IfdOrderOptions & {
|
|||||||
export type ThemeInvestmentOrderOptions = {
|
export type ThemeInvestmentOrderOptions = {
|
||||||
/** Theme ID for the theme investment order. */
|
/** Theme ID for the theme investment order. */
|
||||||
themeId: ThemeId
|
themeId: ThemeId
|
||||||
|
/** Theme set year/month (`theme_set_yyyymm`) from the mobile APK handoff. */
|
||||||
|
themeSetYyyymm: string
|
||||||
|
/** Theme course (`theme_course`) from the mobile APK handoff. */
|
||||||
|
themeCourse: number | string
|
||||||
/** Buy or sell side for the order. */
|
/** Buy or sell side for the order. */
|
||||||
side: TradeSide
|
side: TradeSide
|
||||||
|
/** Account/deposit type used for the theme investment order. */
|
||||||
|
accountType?: AccountType
|
||||||
|
/** Deposit type used for the theme investment order. */
|
||||||
|
depositType?: DepositType
|
||||||
|
/** Component stock orders selected by the mobile theme investment flow. */
|
||||||
|
components: ThemeInvestmentOrderComponent[]
|
||||||
/** Order amount for the theme investment order. */
|
/** Order amount for the theme investment order. */
|
||||||
amount?: number
|
amount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThemeInvestmentOrderComponent = {
|
||||||
|
/** Component stock issue code. */
|
||||||
|
issueCode: IssueCode
|
||||||
|
/** Component order quantity. */
|
||||||
|
quantity: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeInvestmentPreOrderComponent = {
|
||||||
|
/** Component stock issue code selected by the mobile theme investment flow. */
|
||||||
|
issueCode: IssueCode
|
||||||
|
/** Component order quantity from the mobile handoff, when already selected. */
|
||||||
|
quantity?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeInvestmentPreOrderOptions = {
|
||||||
|
/** Theme ID for the theme investment order target. */
|
||||||
|
themeId: ThemeId
|
||||||
|
/** Theme name from the mobile theme investment flow, when available. */
|
||||||
|
themeName?: string
|
||||||
|
/** Exchange code used for all component stocks in the mobile pre-order call. */
|
||||||
|
exchangeCode: MarketCode
|
||||||
|
/** Component stocks selected by the mobile theme investment flow. */
|
||||||
|
components: ThemeInvestmentPreOrderComponent[]
|
||||||
|
}
|
||||||
|
|
||||||
export type PlaceThemeInvestmentOrderOptions = ThemeInvestmentOrderOptions & {
|
export type PlaceThemeInvestmentOrderOptions = ThemeInvestmentOrderOptions & {
|
||||||
/** Explicitly allows sending a live theme investment order. */
|
/** Explicitly allows sending a live theme investment order. */
|
||||||
allowTrading?: true
|
allowTrading?: true
|
||||||
@@ -425,6 +617,8 @@ export interface SbiClientMethodOrderInquiry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SbiClientMethodCashOrder {
|
export interface SbiClientMethodCashOrder {
|
||||||
|
/** Fetches APK cash pre-order information and selectable constraints. */
|
||||||
|
preOrder(options: CashOrderPreOrderOptions): Promise<StockOrderPreOrder>
|
||||||
/** Estimates a cash order without submitting a live order. */
|
/** Estimates a cash order without submitting a live order. */
|
||||||
estimate(options: CashOrderOptions): Promise<OrderPreview>
|
estimate(options: CashOrderOptions): Promise<OrderPreview>
|
||||||
/** Places a live cash order. Requires `allowTrading: true`. */
|
/** Places a live cash order. Requires `allowTrading: true`. */
|
||||||
@@ -442,10 +636,14 @@ export interface SbiClientMethodCashOrder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SbiClientMethodMarginOrder {
|
export interface SbiClientMethodMarginOrder {
|
||||||
|
/** Fetches APK margin-open pre-order information and selectable constraints. */
|
||||||
|
preOrderOpen(options: MarginOpenOrderPreOrderOptions): Promise<StockOrderPreOrder>
|
||||||
/** Estimates a margin-open order without submitting a live order. */
|
/** Estimates a margin-open order without submitting a live order. */
|
||||||
estimateOpen(options: MarginOpenOrderOptions): Promise<OrderPreview>
|
estimateOpen(options: MarginOpenOrderOptions): Promise<OrderPreview>
|
||||||
/** Places a live margin-open order. Requires `allowTrading: true`. */
|
/** Places a live margin-open order. Requires `allowTrading: true`. */
|
||||||
open(options: PlaceMarginOpenOrderOptions): Promise<OrderReceipt>
|
open(options: PlaceMarginOpenOrderOptions): Promise<OrderReceipt>
|
||||||
|
/** Fetches APK margin-close pre-order information and selectable constraints. */
|
||||||
|
preOrderClose(options: MarginCloseOrderPreOrderOptions): Promise<StockOrderPreOrder>
|
||||||
/** Estimates a margin close order without submitting a live order. */
|
/** Estimates a margin close order without submitting a live order. */
|
||||||
estimateClose(options: MarginCloseOrderOptions): Promise<OrderPreview>
|
estimateClose(options: MarginCloseOrderOptions): Promise<OrderPreview>
|
||||||
/** Places a live margin close order. Requires `allowTrading: true`. */
|
/** Places a live margin close order. Requires `allowTrading: true`. */
|
||||||
@@ -458,6 +656,8 @@ export interface SbiClientMethodMarginOrder {
|
|||||||
estimateSummary(options: MarginCloseSummaryOrderOptions): Promise<OrderPreview>
|
estimateSummary(options: MarginCloseSummaryOrderOptions): Promise<OrderPreview>
|
||||||
/** Places a mobile margin close summary order. Requires `allowTrading: true`. */
|
/** Places a mobile margin close summary order. Requires `allowTrading: true`. */
|
||||||
placeSummary(options: PlaceMarginCloseSummaryOrderOptions): Promise<OrderReceipt>
|
placeSummary(options: PlaceMarginCloseSummaryOrderOptions): Promise<OrderReceipt>
|
||||||
|
/** Fetches APK genbiki/genwatashi pre-order information and selectable constraints. */
|
||||||
|
preOrderActualDelivery(options: ActualDeliveryOrderPreOrderOptions): Promise<StockOrderPreOrder>
|
||||||
/** Estimates a genbiki/genwatashi actual-delivery order without submitting a live order. */
|
/** Estimates a genbiki/genwatashi actual-delivery order without submitting a live order. */
|
||||||
estimateActualDelivery(options: ActualDeliveryOrderOptions): Promise<OrderPreview>
|
estimateActualDelivery(options: ActualDeliveryOrderOptions): Promise<OrderPreview>
|
||||||
/** Places a genbiki/genwatashi actual-delivery order. Requires `allowTrading: true`. */
|
/** Places a genbiki/genwatashi actual-delivery order. Requires `allowTrading: true`. */
|
||||||
@@ -474,14 +674,14 @@ export interface SbiClientMethodIfdOrder {
|
|||||||
/** Places a live IFD order correction. Requires `allowTrading: true`. */
|
/** Places a live IFD order correction. Requires `allowTrading: true`. */
|
||||||
placeCorrection(options: PlaceOrderCorrectionOptions): Promise<OrderReceipt>
|
placeCorrection(options: PlaceOrderCorrectionOptions): Promise<OrderReceipt>
|
||||||
/** Estimates an IFD order cancellation without submitting a live cancellation. */
|
/** Estimates an IFD order cancellation without submitting a live cancellation. */
|
||||||
estimateCancel(options: OrderCorrectionOptions): Promise<OrderPreview>
|
estimateCancel(options: OrderCancelOptions): Promise<OrderPreview>
|
||||||
/** Places a live IFD order cancellation. Requires `allowTrading: true`. */
|
/** Places a live IFD order cancellation. Requires `allowTrading: true`. */
|
||||||
placeCancel(options: PlaceOrderCorrectionOptions): Promise<OrderReceipt>
|
placeCancel(options: PlaceOrderCancelOptions): Promise<OrderReceipt>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SbiClientMethodThemeInvestmentOrder {
|
export interface SbiClientMethodThemeInvestmentOrder {
|
||||||
/** Fetches theme investment holdings or order targets. */
|
/** Fetches APK theme investment pre-order targets for selected component stocks. */
|
||||||
list(): Promise<ThemeInvestmentList>
|
list(options: ThemeInvestmentPreOrderOptions): Promise<ThemeInvestmentList>
|
||||||
/** Estimates a theme investment order without submitting a live order. */
|
/** Estimates a theme investment order without submitting a live order. */
|
||||||
estimate(options: ThemeInvestmentOrderOptions): Promise<OrderPreview>
|
estimate(options: ThemeInvestmentOrderOptions): Promise<OrderPreview>
|
||||||
/** Places a live theme investment order. Requires `allowTrading: true`. */
|
/** Places a live theme investment order. Requires `allowTrading: true`. */
|
||||||
|
|||||||
@@ -150,8 +150,20 @@ export type SignedTextValue = {
|
|||||||
sign?: 'positive' | 'negative' | 'zero'
|
sign?: 'positive' | 'negative' | 'zero'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccountType = 'general' | 'specific' | 'nisa' | 'juniorNisa' | 'unknown'
|
export type AccountType =
|
||||||
export type DepositType = 'general' | 'specific' | 'nisa' | 'juniorNisa' | 'unknown'
|
| 'general'
|
||||||
|
| 'specific'
|
||||||
|
| 'growthInvestment'
|
||||||
|
| 'nisa'
|
||||||
|
| 'juniorNisa'
|
||||||
|
| 'unknown'
|
||||||
|
export type DepositType =
|
||||||
|
| 'general'
|
||||||
|
| 'specific'
|
||||||
|
| 'growthInvestment'
|
||||||
|
| 'nisa'
|
||||||
|
| 'juniorNisa'
|
||||||
|
| 'unknown'
|
||||||
export type TradeSide = 'buy' | 'sell'
|
export type TradeSide = 'buy' | 'sell'
|
||||||
export type MarginTradeSide = 'buy' | 'sell'
|
export type MarginTradeSide = 'buy' | 'sell'
|
||||||
export type OrderStatus = 'open' | 'executed' | 'cancelled' | 'expired' | 'rejected' | 'unknown'
|
export type OrderStatus = 'open' | 'executed' | 'cancelled' | 'expired' | 'rejected' | 'unknown'
|
||||||
@@ -561,6 +573,75 @@ export type OrderList = {
|
|||||||
error?: SbiMethodError
|
error?: SbiMethodError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OrderCorrectionPreOrderDetail = {
|
||||||
|
exchangeName?: string
|
||||||
|
marketLoanKbn?: string
|
||||||
|
marketIppanLoanKbn?: string
|
||||||
|
currentPrice?: CurrencyAmount
|
||||||
|
tradeColorFlag?: string
|
||||||
|
priceTick?: string
|
||||||
|
priceTickText?: string
|
||||||
|
tradeTime?: string
|
||||||
|
changeText?: string
|
||||||
|
volumeText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderCorrectionPreOrder = {
|
||||||
|
issue: IssueRef
|
||||||
|
tradeTitle?: string
|
||||||
|
buyingPowerTotal?: CurrencyAmount
|
||||||
|
controlledStockCode?: string
|
||||||
|
hasTradeWarning?: boolean
|
||||||
|
deficitMessageFlag?: string
|
||||||
|
deficitMessage?: string
|
||||||
|
details: OrderCorrectionPreOrderDetail[]
|
||||||
|
orderNumber?: string
|
||||||
|
orderId?: string
|
||||||
|
primaryOrderMethod?: string
|
||||||
|
primaryTriggerZone?: string
|
||||||
|
primaryTriggerPrice?: number | null
|
||||||
|
status?: string
|
||||||
|
statusText?: string
|
||||||
|
tradeId?: string
|
||||||
|
tradeName?: string
|
||||||
|
quantity?: number | null
|
||||||
|
quantityText?: string
|
||||||
|
orderLimit?: string
|
||||||
|
orderLimitText?: string
|
||||||
|
priceSteps: StockOrderPreOrderPriceStep[]
|
||||||
|
sessionRange?: string
|
||||||
|
inputDateText?: string
|
||||||
|
primaryOrderTerm?: string
|
||||||
|
nonSpecificTradeText?: string
|
||||||
|
marketName?: string
|
||||||
|
rbeOrderStatus?: string
|
||||||
|
priceCondition?: string
|
||||||
|
price?: number | null
|
||||||
|
priceAmount?: CurrencyAmount
|
||||||
|
exchangeName?: string
|
||||||
|
transId?: string
|
||||||
|
ptsDayNightFlag?: string
|
||||||
|
smallTickFlag?: string
|
||||||
|
juniorBuyingPowerTotal?: CurrencyAmount
|
||||||
|
secondaryPriceCondition?: string
|
||||||
|
secondaryPrice?: number | null
|
||||||
|
secondaryPriceAmount?: CurrencyAmount
|
||||||
|
autoOrderKind?: string
|
||||||
|
autoOrderNumber?: string
|
||||||
|
autoOrderInputDate?: string
|
||||||
|
secondaryOrderMethod?: string
|
||||||
|
secondaryTriggerZone?: string
|
||||||
|
secondaryTriggerPrice?: number | null
|
||||||
|
secondaryOrderCondition?: string
|
||||||
|
secondaryLimitPrice?: number | null
|
||||||
|
secondaryLimitPriceAmount?: CurrencyAmount
|
||||||
|
secondaryOrderTerm?: string
|
||||||
|
secondaryOcoPriceCondition?: string
|
||||||
|
secondaryOcoPrice?: number | null
|
||||||
|
secondaryOcoPriceAmount?: CurrencyAmount
|
||||||
|
exchangeList?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type OrderPreview = {
|
export type OrderPreview = {
|
||||||
issue: IssueRef
|
issue: IssueRef
|
||||||
side: TradeSide
|
side: TradeSide
|
||||||
@@ -572,6 +653,89 @@ export type OrderPreview = {
|
|||||||
warnings: string[]
|
warnings: string[]
|
||||||
confirmationId?: string
|
confirmationId?: string
|
||||||
message?: string
|
message?: string
|
||||||
|
correction?: OrderCorrectionPreOrder
|
||||||
|
error?: SbiMethodError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StockOrderPreOrderPriceStep = {
|
||||||
|
from?: CurrencyAmount
|
||||||
|
to?: CurrencyAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StockOrderPreOrderPaymentLimit = {
|
||||||
|
text?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StockOrderPreOrderMarginTradeType =
|
||||||
|
| 'standard'
|
||||||
|
| 'generalBuy'
|
||||||
|
| 'generalSellShort'
|
||||||
|
| 'generalSellInventoryLimited'
|
||||||
|
| 'generalSellInventoryUnlimited'
|
||||||
|
| 'day'
|
||||||
|
| 'hyper'
|
||||||
|
|
||||||
|
export type StockOrderPreOrder = {
|
||||||
|
issue: IssueRef
|
||||||
|
tradeTitle?: string
|
||||||
|
buyingPowerTotal?: CurrencyAmount
|
||||||
|
controlledStockCode?: string
|
||||||
|
hasTradeWarning?: boolean
|
||||||
|
market?: MarketCode
|
||||||
|
exchangeList?: string
|
||||||
|
exchangeListName?: string
|
||||||
|
exchangeListIndexFlag?: string
|
||||||
|
marketLoanKbn?: string
|
||||||
|
marketIppanLoanKbn?: string
|
||||||
|
currentPrice?: CurrencyAmount
|
||||||
|
tradeColorFlag?: string
|
||||||
|
priceTick?: string
|
||||||
|
priceTickText?: string
|
||||||
|
tradeTime?: string
|
||||||
|
changeText?: string
|
||||||
|
volume?: number | null
|
||||||
|
lotSize?: number | null
|
||||||
|
priceSteps: StockOrderPreOrderPriceStep[]
|
||||||
|
sessionRange?: string
|
||||||
|
basePrice?: CurrencyAmount
|
||||||
|
orderTerms: string[]
|
||||||
|
orderTermDates: string[]
|
||||||
|
paymentLimits: StockOrderPreOrderPaymentLimit[]
|
||||||
|
nonSpecificTradeText?: string
|
||||||
|
paymentLimitText?: string
|
||||||
|
acquisitionPrice?: CurrencyAmount
|
||||||
|
position?: number | null
|
||||||
|
unexecutedQuantity?: number | null
|
||||||
|
lotSize2?: number | null
|
||||||
|
ptsDayNightFlag?: string
|
||||||
|
sorServiceType?: string
|
||||||
|
nisa?: {
|
||||||
|
serviceKbn?: string
|
||||||
|
buyLimit?: CurrencyAmount
|
||||||
|
growthServiceKbn?: string
|
||||||
|
juniorServiceKbn?: string
|
||||||
|
juniorBuyLimit?: CurrencyAmount
|
||||||
|
juniorBuyingPowerTotal?: CurrencyAmount
|
||||||
|
}
|
||||||
|
smallTickFlag?: string
|
||||||
|
margin?: {
|
||||||
|
tradeTypes?: StockOrderPreOrderMarginTradeType[]
|
||||||
|
ippanShort?: string
|
||||||
|
ippanLong?: string
|
||||||
|
dayBuy?: string
|
||||||
|
daySell?: string
|
||||||
|
premiumShortSelling?: string
|
||||||
|
premiumFee?: CurrencyAmount
|
||||||
|
ippanPaymentLimit?: string
|
||||||
|
positionStatus?: string
|
||||||
|
}
|
||||||
|
sKabu?: {
|
||||||
|
code?: string
|
||||||
|
available?: boolean
|
||||||
|
}
|
||||||
|
deficitMessageFlag?: string
|
||||||
|
deficitMessage?: string
|
||||||
error?: SbiMethodError
|
error?: SbiMethodError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,12 +750,34 @@ export type OrderReceipt = {
|
|||||||
export type ThemeInvestment = {
|
export type ThemeInvestment = {
|
||||||
id: ThemeId
|
id: ThemeId
|
||||||
name: string
|
name: string
|
||||||
issues: IssueRef[]
|
issues: ThemeInvestmentIssue[]
|
||||||
minimumAmount?: CurrencyAmount
|
minimumAmount?: CurrencyAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThemeInvestmentIssue = IssueRef & {
|
||||||
|
controlledStockCode?: string
|
||||||
|
hasTradeWarning?: boolean
|
||||||
|
nisaServiceKbn?: string
|
||||||
|
juniorNisaServiceKbn?: string
|
||||||
|
growthNisaServiceKbn?: string
|
||||||
|
sKabuCode?: string
|
||||||
|
sKabuAvailable?: boolean
|
||||||
|
lotSize?: number | null
|
||||||
|
currentPrice?: CurrencyAmount
|
||||||
|
tradeColorFlag?: string
|
||||||
|
priceTick?: string
|
||||||
|
priceTickText?: string
|
||||||
|
tradeTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ThemeInvestmentList = {
|
export type ThemeInvestmentList = {
|
||||||
themes: ThemeInvestment[]
|
themes: ThemeInvestment[]
|
||||||
|
buyingPowerTotal?: CurrencyAmount
|
||||||
|
isaBuyLimit?: CurrencyAmount
|
||||||
|
juniorNisaBuyLimit?: CurrencyAmount
|
||||||
|
buyingPowerTotalJuniorNisa?: CurrencyAmount
|
||||||
|
deficitMessage?: string
|
||||||
|
deficitMessageFlag?: string
|
||||||
error?: SbiMethodError
|
error?: SbiMethodError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export default defineConfig({
|
|||||||
verify: {
|
verify: {
|
||||||
command: ['vp check', 'vp run typecheck:all', 'vp run app:build'],
|
command: ['vp check', 'vp run typecheck:all', 'vp run app:build'],
|
||||||
},
|
},
|
||||||
|
'docker:build':
|
||||||
|
'docker build --no-cache -f apps/csbie/Dockerfile -t git.yutakobayashi.com/nakasyou/csbi:latest .',
|
||||||
|
'docker:run':
|
||||||
|
'docker run --rm -p 8787:8787 --env-file .env -v "$PWD/data:/app/data" git.yutakobayashi.com/nakasyou/csbi:latest',
|
||||||
|
'docker:tag':
|
||||||
|
'docker tag git.yutakobayashi.com/nakasyou/csbi:latest git.yutakobayashi.com/nakasyou/csbi:latest',
|
||||||
|
'docker:push': 'docker push git.yutakobayashi.com/nakasyou/csbi:latest',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user