diff --git a/src/derivatives/derivativeExecutionService.ts b/src/derivatives/derivativeExecutionService.ts index 90a3b0a..9f2ed10 100644 --- a/src/derivatives/derivativeExecutionService.ts +++ b/src/derivatives/derivativeExecutionService.ts @@ -5,7 +5,10 @@ import { readdir } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; -import type { CanonicalEquityIntent } from "#src/equities/equityOrder.js"; +import { + canonicalEquityIntentSchema, + type CanonicalEquityIntent, +} from "#src/equities/equityOrder.js"; import { canonicalForexIntentSchema, type CanonicalForexIntent } from "#src/forex/forexOrder.js"; import type { CanonicalSingleOptionIntent } from "#src/options/optionOrder.js"; import type { DerivativeDiscoveryClient } from "./derivativeDiscovery.js"; @@ -316,22 +319,6 @@ const operationSchema = z.strictObject({ createdAt: z.iso.datetime(), latestTransitionAt: z.iso.datetime(), }) as unknown as z.ZodType; -const executionEquityIntentSchema = z.strictObject({ - contract: z.strictObject({ - conid: z.number().int().positive(), - assetClass: z.literal("STK"), - symbol: z.string().regex(/^[A-Z0-9][A-Z0-9 .-]{0,31}$/u), - exchange: z.literal("SMART"), - primaryExchange: z.string().min(1).max(32), - currency: z.literal("USD"), - }), - side: z.enum(["BUY", "SELL"]), - quantity: z.number().int().positive(), - tif: z.enum(["DAY", "GTC"]), - session: z.enum(["REGULAR", "OVERNIGHT"]), - orderType: z.literal("LMT"), - limit: z.number().positive(), -}); const executionOptionIntentSchema = z.strictObject({ contract: z.strictObject({ conid: z.number().int().positive(), @@ -377,7 +364,7 @@ const submissionSchema = z.discriminatedUnion("operationKind", [ ...submissionBaseSchema, operationKind: z.literal("single"), canonicalIntent: z.union([ - executionEquityIntentSchema, + canonicalEquityIntentSchema, canonicalForexIntentSchema, executionOptionIntentSchema, ]), diff --git a/src/equities/equityOrder.ts b/src/equities/equityOrder.ts index 323011d..763b814 100644 --- a/src/equities/equityOrder.ts +++ b/src/equities/equityOrder.ts @@ -1,3 +1,4 @@ +import { z } from "zod"; import type { SingleOrderGateway, SingleOrderPreviewResult, @@ -31,3 +32,35 @@ export type EquityTradingDiagnostics = SingleOrderTradingDiagnostics; export interface EquityGatewayClient extends SingleOrderGateway { resolveContract(symbol: string): Promise; } + +const contractSchema = z.strictObject({ + conid: z.number().int().positive(), + assetClass: z.literal("STK"), + symbol: z.string().regex(/^[A-Z0-9][A-Z0-9 .-]{0,31}$/), + exchange: z.literal("SMART"), + primaryExchange: z.string().min(1).max(32), + currency: z.literal("USD"), +}); +const sharedIntentFields = { + contract: contractSchema, + side: z.enum(["BUY", "SELL"]), + quantity: z.number().int().positive(), + tif: z.enum(["DAY", "GTC"]), + session: z.enum(["REGULAR", "OVERNIGHT"]), +}; +/** The one schema of a canonical equity intent: a LIMIT or a native STOP order. */ +export const canonicalEquityIntentSchema: z.ZodType = z.discriminatedUnion( + "orderType", + [ + z.strictObject({ + ...sharedIntentFields, + orderType: z.literal("LMT"), + limit: z.number().positive(), + }), + z.strictObject({ + ...sharedIntentFields, + orderType: z.literal("STP"), + stopPrice: z.number().positive(), + }), + ] +); diff --git a/src/equities/equityOrderService.ts b/src/equities/equityOrderService.ts index 3203609..c171cf1 100644 --- a/src/equities/equityOrderService.ts +++ b/src/equities/equityOrderService.ts @@ -1,6 +1,5 @@ import { homedir } from "node:os"; import { join } from "node:path"; -import { z } from "zod"; import type { SingleSubmissionRecord } from "#src/derivatives/derivativeExecutionService.js"; import { FileSingleOrderPreviewStore, @@ -16,7 +15,11 @@ import { type SingleOrderSubmissionRecord, type SingleOrderSubmissionStore, } from "#src/orders/singleOrderWorkflow.js"; -import type { CanonicalEquityIntent, EquityGatewayClient } from "./equityOrder.js"; +import { + canonicalEquityIntentSchema, + type CanonicalEquityIntent, + type EquityGatewayClient, +} from "./equityOrder.js"; export type NormalizedEquityTerms = | { readonly orderType: "LIMIT"; readonly limit: number } @@ -88,34 +91,6 @@ export type EquitySubmissionDto = SingleOrderSubmissionDto; export type EquitySubmissionStore = SingleOrderSubmissionStore; -const contractSchema = z.strictObject({ - conid: z.number().int().positive(), - assetClass: z.literal("STK"), - symbol: z.string().regex(/^[A-Z0-9][A-Z0-9 .-]{0,31}$/), - exchange: z.literal("SMART"), - primaryExchange: z.string().min(1).max(32), - currency: z.literal("USD"), -}); -const sharedIntentFields = { - contract: contractSchema, - side: z.enum(["BUY", "SELL"]), - quantity: z.number().int().positive(), - tif: z.enum(["DAY", "GTC"]), - session: z.enum(["REGULAR", "OVERNIGHT"]), -}; -export const canonicalEquityIntentSchema = z.discriminatedUnion("orderType", [ - z.strictObject({ - ...sharedIntentFields, - orderType: z.literal("LMT"), - limit: z.number().positive(), - }), - z.strictObject({ - ...sharedIntentFields, - orderType: z.literal("STP"), - stopPrice: z.number().positive(), - }), -]); - export class InMemoryEquityPreviewStore extends InMemorySingleOrderPreviewStore {} export class InMemoryEquitySubmissionStore extends InMemorySingleOrderSubmissionStore {} diff --git a/test/equities/equityOrderService.test.ts b/test/equities/equityOrderService.test.ts index 017a107..7f9a5c0 100644 --- a/test/equities/equityOrderService.test.ts +++ b/test/equities/equityOrderService.test.ts @@ -356,6 +356,28 @@ test("creating a preview prunes abandoned expired preview files", async () => { } }); +test("the shared execution store persists an equity STOP submission", async () => { + const root = await mkdtemp(join(tmpdir(), "huskly-equity-stop-")); + try { + const gateway = new Gateway(); + const submissions = new FileEquitySubmissionStore(root); + const value = service(gateway, new InMemoryEquityPreviewStore(), submissions).value; + const result = await preview(value, "STOP"); + const submitted = await value.submit({ + previewId: result.previewId, + operator: "operator-7", + confirm: true, + }); + assert.equal(submitted.operation.operationId, "operation-1"); + assert.equal(gateway.createCalls.length, 1); + const stored = await submissions.load(result.previewId); + assert.deepEqual(stored?.canonicalIntent, result.order); + assert.equal(stored.state, "operation_known"); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("shared lifecycle storage indexes an equity operation for generic tools", async () => { const root = await mkdtemp(join(tmpdir(), "huskly-equity-lifecycle-")); try {