@sudowealth/schwab-api
Version:
TypeScript client for Charles Schwab API with OAuth support, market data, trading functionality, and complete type safety
437 lines (436 loc) • 15.2 kB
JavaScript
import { z } from 'zod';
import { mergeShapes } from '../../utils/schema-utils';
import { assetType, AccountAPIOptionDeliverable } from '../shared';
// Create a flexible date schema for query parameters
const flexibleDateSchema = (daysOffset, description) => z
.preprocess((val) => {
// Handle empty strings and undefined
if (val === '' || val === undefined || val === null) {
// Generate default date in full ISO format
const date = new Date();
date.setDate(date.getDate() + daysOffset);
// For start dates, set to beginning of day; for end dates, set to end of day
if (daysOffset < 0) {
// Start date - beginning of day
date.setHours(0, 0, 0, 0);
}
else {
// End date - end of day
date.setHours(23, 59, 59, 999);
}
return date.toISOString();
}
// Handle YYYY-MM-DD format - convert to full ISO datetime
if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(val)) {
const date = new Date(val + 'T00:00:00.000Z');
return date.toISOString();
}
// Handle full ISO datetime - ensure proper format
if (typeof val === 'string' && val.includes('T')) {
return new Date(val).toISOString();
}
return val;
}, z.string().describe(description))
.optional();
const session = z.enum(['NORMAL', 'AM', 'PM', 'SEAMLESS']);
const duration = z.enum([
'DAY',
'GOOD_TILL_CANCEL',
'FILL_OR_KILL',
'IMMEDIATE_OR_CANCEL',
'END_OF_WEEK',
'END_OF_MONTH',
'NEXT_END_OF_MONTH',
'UNKNOWN',
]);
const orderTypeRequest = z.enum([
'MARKET',
'LIMIT',
'STOP',
'STOP_LIMIT',
'TRAILING_STOP',
'CABINET',
'NON_MARKETABLE',
'MARKET_ON_CLOSE',
'EXERCISE',
'TRAILING_STOP_LIMIT',
'NET_DEBIT',
'NET_CREDIT',
'NET_ZERO',
'LIMIT_ON_CLOSE',
]);
const orderType = z.enum([...orderTypeRequest.options, 'UNKNOWN']);
const complexOrderStrategyType = z.enum([
'NONE',
'COVERED',
'VERTICAL',
'BACK_RATIO',
'CALENDAR',
'DIAGONAL',
'STRADDLE',
'STRANGLE',
'COLLAR_SYNTHETIC',
'BUTTERFLY',
'CONDOR',
'IRON_CONDOR',
'VERTICAL_ROLL',
'COLLAR_WITH_STOCK',
'DOUBLE_DIAGONAL',
'UNBALANCED_BUTTERFLY',
'UNBALANCED_CONDOR',
'UNBALANCED_IRON_CONDOR',
'UNBALANCED_VERTICAL_ROLL',
'MUTUAL_FUND_SWAP',
'CUSTOM',
]);
const requestedDestination = z.enum([
'INET',
'ECN_ARCA',
'CBOE',
'AMEX',
'PHLX',
'ISE',
'BOX',
'NYSE',
'NASDAQ',
'BATS',
'C2',
'AUTO',
]);
const stopPriceLinkBasis = z.enum([
'MANUAL',
'BASE',
'TRIGGER',
'LAST',
'BID',
'ASK',
'ASK_BID',
'MARK',
'AVERAGE',
]);
const stopPriceLinkType = z.enum(['VALUE', 'PERCENT', 'TICK']);
const stopType = z.enum(['STANDARD', 'BID', 'ASK', 'LAST', 'MARK']);
const priceLinkBasis = z.enum([
'MANUAL',
'BASE',
'TRIGGER',
'LAST',
'BID',
'ASK',
'ASK_BID',
'MARK',
'AVERAGE',
]);
const priceLinkType = z.enum(['VALUE', 'PERCENT', 'TICK']);
const taxLotMethod = z.enum([
'FIFO',
'LIFO',
'HIGH_COST',
'LOW_COST',
'AVERAGE_COST',
'SPECIFIC_LOT',
'LOSS_HARVESTER',
]);
const specialInstruction = z.enum([
'ALL_OR_NONE',
'DO_NOT_REDUCE',
'ALL_OR_NONE_DO_NOT_REDUCE',
]);
const orderStrategyType = z.enum([
'SINGLE',
'CANCEL',
'RECALL',
'PAIR',
'FLATTEN',
'TWO_DAY_SWAP',
'BLAST_ALL',
'OCO',
'TRIGGER',
]);
const instruction = z.enum([
'BUY',
'SELL',
'BUY_TO_COVER',
'SELL_SHORT',
'BUY_TO_OPEN',
'BUY_TO_CLOSE',
'SELL_TO_OPEN',
'SELL_TO_CLOSE',
'EXCHANGE',
'SELL_SHORT_EXEMPT',
]);
const activityType = z.enum(['EXECUTION', 'ORDER_ACTION']);
const executionType = z.enum(['FILL']);
const orderLegType = z.enum([
'EQUITY',
'OPTION',
'INDEX',
'MUTUAL_FUND',
'CASH_EQUIVALENT',
'FIXED_INCOME',
'CURRENCY',
'COLLECTIVE_INVESTMENT',
]);
const positionEffect = z.enum(['OPENING', 'CLOSING', 'AUTOMATIC']);
const quantityType = z.enum(['ALL_SHARES', 'DOLLARS', 'SHARES']);
const divCapGains = z.enum(['REINVEST', 'PAYOUT']);
const status = z.enum([
'AWAITING_PARENT_ORDER',
'AWAITING_CONDITION',
'AWAITING_STOP_CONDITION',
'AWAITING_MANUAL_REVIEW',
'ACCEPTED',
'AWAITING_UR_OUT',
'PENDING_ACTIVATION',
'QUEUED',
'WORKING',
'REJECTED',
'PENDING_CANCEL',
'CANCELED',
'PENDING_REPLACE',
'REPLACED',
'FILLED',
'EXPIRED',
'NEW',
'AWAITING_RELEASE_TIME',
'PENDING_ACKNOWLEDGEMENT',
'PENDING_RECALL',
'UNKNOWN',
]);
const AccountsBaseInstrument = z.object({
assetType: assetType,
cusip: z.string(),
symbol: z.string(),
description: z.string(),
instrumentId: z.number().int().optional(), // Assuming int based on spec ($int64)
netChange: z.number().optional(),
});
// // Define placeholder schemas for the individual account instrument types
const AccountCashEquivalent = AccountsBaseInstrument.extend({
assetType: z.literal('CASH_EQUIVALENT'),
type: z.enum(['SWEEP_VEHICLE', 'SAVINGS', 'MONEY_MARKET_FUND', 'UNKNOWN']),
underlyingSymbol: z.string(),
});
const AccountEquity = AccountsBaseInstrument.extend({
assetType: z.literal('EQUITY'),
});
const AccountFixedIncome = AccountsBaseInstrument.extend({
assetType: z.literal('FIXED_INCOME'),
maturityDate: z.string().datetime(),
factor: z.number(),
variableRate: z.number(),
});
const AccountMutualFund = AccountsBaseInstrument.extend({
assetType: z.literal('MUTUAL_FUND'),
});
const AccountOption = AccountsBaseInstrument.extend({
assetType: z.literal('OPTION'),
optionDeliverables: z.array(z.lazy(() => AccountAPIOptionDeliverable)),
putCall: z.enum(['PUT', 'CALL', 'UNKNOWN']),
optionMultiplier: z.number().int(),
type: z.enum(['VANILLA', 'BINARY', 'BARRIER', 'UNKNOWN']),
});
const AccountFuture = AccountsBaseInstrument.extend({
assetType: z.literal('FUTURE'),
expirationDate: z.string().datetime().optional(),
activeContract: z.boolean().default(false),
});
const AccountForex = AccountsBaseInstrument.extend({
assetType: z.literal('FOREX'),
});
const AccountIndex = AccountsBaseInstrument.extend({
assetType: z.literal('INDEX'),
});
const AccountProduct = AccountsBaseInstrument.extend({
assetType: z.literal('PRODUCT'),
});
const AccountCurrency = AccountsBaseInstrument.extend({
assetType: z.literal('CURRENCY'),
});
const AccountCollectiveInvestment = AccountsBaseInstrument.extend({
assetType: z.literal('COLLECTIVE_INVESTMENT'),
});
export const AccountsInstrument = z.discriminatedUnion('assetType', [
AccountCashEquivalent,
AccountEquity,
AccountFixedIncome,
AccountMutualFund,
AccountOption,
AccountFuture,
AccountForex,
AccountIndex,
AccountProduct,
AccountCurrency,
AccountCollectiveInvestment,
]);
const ExecutionLeg = z.object({
legId: z.number().int(),
price: z.number(),
quantity: z.number(),
mismarkedQuantity: z.number(),
instrumentId: z.number().int(), // Assuming instrumentId is an int based on context
time: z.string().datetime(),
});
const OrderActivity = z.object({
activityType: activityType,
executionType: executionType, // Optional based on activityType = EXECUTION
quantity: z.number(),
orderRemainingQuantity: z.number(),
executionLegs: z.array(ExecutionLeg),
});
const OrderLegCollection = z.object({
orderLegType: orderLegType,
legId: z.number().int(),
instrument: z.lazy(() => AccountsInstrument), // Use lazy for potential circular dependency
instruction: instruction,
positionEffect: positionEffect,
quantity: z.number(),
quantityType: quantityType,
divCapGains: divCapGains,
toSymbol: z.string(),
});
const OrderRequest = z.object({
session: session,
duration: duration,
orderType: orderTypeRequest,
cancelTime: z.string().datetime(),
complexOrderStrategyType: complexOrderStrategyType,
quantity: z.number(),
filledQuantity: z.number(), // Typically not part of a request, but included per spec
remainingQuantity: z.number(), // Typically not part of a request, but included per spec
destinationLinkName: z.string(),
releaseTime: z.string().datetime(),
stopPrice: z.number(),
stopPriceLinkBasis: stopPriceLinkBasis,
stopPriceLinkType: stopPriceLinkType,
stopPriceOffset: z.number(),
stopType: stopType,
priceLinkBasis: priceLinkBasis,
priceLinkType: priceLinkType,
price: z.number(),
taxLotMethod: taxLotMethod,
orderLegCollection: z.array(OrderLegCollection),
activationPrice: z.number(),
specialInstruction: specialInstruction,
orderStrategyType: orderStrategyType,
orderId: z.number().int(), // Optional for new orders
cancelable: z.boolean().default(false), // Typically not part of a request
editable: z.boolean().default(false), // Typically not part of a request
status: status, // Typically not part of a request
enteredTime: z.string().datetime(), // Typically set by server
closeTime: z.string().datetime(), // Typically set by server
accountNumber: z.string(), // Must match path param, but often optional in body?
orderActivityCollection: z.array(OrderActivity), // Typically not part of a request
replacingOrderCollection: z.array(z.object({})), // Placeholder
childOrderStrategies: z.array(z.object({})), // Placeholder
statusDescription: z.string(), // Typically not part of a request
});
const Order = z.object({
session: session,
duration: duration,
orderType: orderType,
cancelTime: z.string().datetime(),
complexOrderStrategyType: complexOrderStrategyType,
quantity: z.number(),
filledQuantity: z.number(),
remainingQuantity: z.number(),
requestedDestination: requestedDestination,
destinationLinkName: z.string(),
releaseTime: z.string().datetime(),
stopPrice: z.number(),
stopPriceLinkBasis: stopPriceLinkBasis,
stopPriceLinkType: stopPriceLinkType,
stopPriceOffset: z.number(),
stopType: stopType,
priceLinkBasis: priceLinkBasis,
priceLinkType: priceLinkType,
price: z.number(),
taxLotMethod: taxLotMethod,
orderLegCollection: z.array(OrderLegCollection),
activationPrice: z.number(),
specialInstruction: specialInstruction,
orderStrategyType: orderStrategyType,
orderId: z.number().int(),
cancelable: z.boolean().default(false),
editable: z.boolean().default(false),
status: status,
enteredTime: z.string().datetime(),
closeTime: z.string().datetime(),
tag: z.string(),
accountNumber: z.number().int(),
orderActivityCollection: z.array(OrderActivity),
replacingOrderCollection: z.array(z.object({})), // Placeholder for unknown structure
childOrderStrategies: z.array(z.object({})), // Placeholder for unknown structure
statusDescription: z.string(),
});
export const GetOrdersResponseBody = z.array(Order);
// --- GET /orders endpoint schemas ---
// Path Parameters Schema for GET /orders (no path params)
export const GetOrdersPathParams = z.object({});
// Query Parameters Schema for GET /orders
export const GetOrdersQueryParams = z.object({
maxResults: z
.number()
.int()
.max(3000)
.optional()
.describe('The maximum number of orders to return'),
fromEnteredTime: flexibleDateSchema(-30, 'Start date to search for orders'),
toEnteredTime: flexibleDateSchema(1, 'End date to search for orders'),
status: status
.optional()
.describe('Specifies that only orders of this status should be returned'),
});
// Request Params Schema for GET /orders (merged path + query params)
export const GetOrdersParams = z.object(mergeShapes(GetOrdersQueryParams.shape, GetOrdersPathParams.shape));
// Response Body Schema for GET /orders
export const GetOrdersResponse = z.array(Order);
// --- GET /accounts/{accountNumber}/orders endpoint schemas ---
// Path Parameters Schema for GET /accounts/{accountNumber}/orders
export const GetOrdersByAccountPathParams = z.object({
accountNumber: z.string().describe('Encrypted account number'),
});
export const GetOrdersByAccountQueryParams = z.object({
maxResults: z
.number()
.int()
.max(3000)
.optional()
.describe('The maximum number of orders to return'),
fromEnteredTime: flexibleDateSchema(-60, 'Start date to search for orders'),
toEnteredTime: flexibleDateSchema(1, 'End date to search for orders'),
status: status
.optional()
.describe('Specifies that only orders of this status should be returned'),
});
export const GetOrdersByAccountParams = z.object(mergeShapes(GetOrdersByAccountQueryParams.shape, GetOrdersByAccountPathParams.shape));
export const GetOrdersByAccountResponse = z.array(Order);
export const PlaceOrderPathParams = GetOrdersByAccountPathParams;
export const PlaceOrderRequestBody = OrderRequest;
export const PlaceOrderParams = z.object(mergeShapes(PlaceOrderPathParams.shape, PlaceOrderRequestBody.shape));
export const PlaceOrderResponse = z.object({}).passthrough();
// --- GET /accounts/{accountNumber}/orders/{orderId} endpoint schemas ---
// Path Parameters Schema for GET /accounts/{accountNumber}/orders/{orderId}
export const GetOrderByIdPathParams = z.object({
accountNumber: z.string().describe('Encrypted account number'),
orderId: z.number().int().describe('The ID of the order being retrieved.'),
});
// Query Parameters Schema for GET /accounts/{accountNumber}/orders/{orderId} (no query params)
export const GetOrderByIdQueryParams = z.object({});
// Request Params Schema for GET /accounts/{accountNumber}/orders/{orderId} (merged path + query params)
export const GetOrderByIdParams = z.object(mergeShapes(GetOrderByIdQueryParams.shape, GetOrderByIdPathParams.shape));
// Response Body Schema for GET /accounts/{accountNumber}/orders/{orderId}
export const GetOrderByIdResponse = Order;
// --- DELETE /accounts/{accountNumber}/orders/{orderId} endpoint schemas ---
// Path Parameters Schema for DELETE /accounts/{accountNumber}/orders/{orderId}
export const CancelOrderPathParams = GetOrderByIdPathParams;
// Query Parameters Schema for DELETE /accounts/{accountNumber}/orders/{orderId} (no query params)
export const CancelOrderQueryParams = z.object({});
// Request Params Schema for DELETE /accounts/{accountNumber}/orders/{orderId} (merged path + query params)
export const CancelOrderParams = z.object(mergeShapes(CancelOrderQueryParams.shape, CancelOrderPathParams.shape));
// Response Body Schema for DELETE /accounts/{accountNumber}/orders/{orderId}
export const CancelOrderResponse = z.object({}).passthrough();
export const ReplaceOrderPathParams = GetOrderByIdPathParams;
export const ReplaceOrderRequestBody = OrderRequest;
export const ReplaceOrderParams = z.object(mergeShapes(ReplaceOrderPathParams.shape, ReplaceOrderRequestBody.shape));
export const ReplaceOrderResponse = z.object({}).passthrough();