@adaptic/utils 0.0.914 → 0.0.916

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5789,6 +5789,24 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
5789
5789
  account: auth.adapticAccountId || "direct",
5790
5790
  });
5791
5791
  if (useLimitOrders) {
5792
+ // Cancel all existing orders first when requested, to free up qty_available
5793
+ // for the limit close orders. Without this, existing trailing stops and
5794
+ // pending orders reduce qty_available, causing "insufficient qty" errors.
5795
+ if (cancel_orders) {
5796
+ try {
5797
+ await cancelAllOrders$1(auth);
5798
+ getLogger().info("Canceled all open orders before placing limit close orders", {
5799
+ account: auth.adapticAccountId || "direct",
5800
+ });
5801
+ }
5802
+ catch (cancelError) {
5803
+ getLogger().warn(`Failed to cancel orders before limit closure: ${cancelError instanceof Error ? cancelError.message : String(cancelError)}`, {
5804
+ account: auth.adapticAccountId || "direct",
5805
+ type: "warn",
5806
+ });
5807
+ // Continue with closure attempt even if cancel failed
5808
+ }
5809
+ }
5792
5810
  const positions = await fetchAllPositions(auth);
5793
5811
  if (positions.length === 0) {
5794
5812
  getLogger().info("No positions to close", {
@@ -5799,13 +5817,10 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
5799
5817
  getLogger().info(`Found ${positions.length} positions to close`, {
5800
5818
  account: auth.adapticAccountId || "direct",
5801
5819
  });
5802
- const alpacaAuth = {
5803
- type: "LIVE",
5804
- alpacaApiKey: process.env.ALPACA_API_KEY,
5805
- alpacaApiSecret: process.env.ALPACA_API_SECRET || process.env.ALPACA_SECRET_KEY,
5806
- };
5820
+ // Use the passed auth for quote fetching (not hardcoded env vars)
5821
+ // so multi-account setups use the correct credentials per account
5807
5822
  const symbols = positions.map((position) => position.symbol);
5808
- const quotesResponse = await getLatestQuotes$1(alpacaAuth, { symbols });
5823
+ const quotesResponse = await getLatestQuotes$1(auth, { symbols });
5809
5824
  const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
5810
5825
  if (lengthOfQuotes === 0) {
5811
5826
  getLogger().error("No quotes available for positions, received 0 quotes", {
@@ -5815,11 +5830,12 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
5815
5830
  return [];
5816
5831
  }
5817
5832
  if (lengthOfQuotes !== positions.length) {
5818
- getLogger().warn(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, {
5833
+ getLogger().warn(`Received ${lengthOfQuotes} quotes for ${positions.length} positions (expected ${positions.length}), proceeding with available quotes`, {
5819
5834
  account: auth.adapticAccountId || "direct",
5820
5835
  type: "warn",
5821
5836
  });
5822
- return [];
5837
+ // Continue with available quotes instead of aborting — some symbols may
5838
+ // lack quotes (halted, delisted) but other positions should still close
5823
5839
  }
5824
5840
  for (const position of positions) {
5825
5841
  const quote = quotesResponse.quotes[position.symbol];
@@ -7161,18 +7177,29 @@ const fetchLastTrade = async (symbol, options) => {
7161
7177
  }
7162
7178
  const apiKey = options?.apiKey || MASSIVE_API_KEY;
7163
7179
  validateMassiveApiKey$1(apiKey);
7164
- const baseUrl = `https://api.massive.com/v2/last/trade/${encodeURIComponent(symbol)}`;
7180
+ const baseUrl = `https://api.massive.com/v3/trades/${encodeURIComponent(symbol)}`;
7165
7181
  const params = new URLSearchParams({
7166
7182
  apiKey,
7183
+ limit: "1",
7184
+ sort: "timestamp",
7185
+ order: "desc",
7167
7186
  });
7168
7187
  return massiveLimit(async () => {
7169
7188
  try {
7170
7189
  const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
7171
- const data = await response.json();
7172
- if (!MASSIVE_VALID_STATUSES.has(data.status) || !data.results) {
7173
- throw new Error(`Massive.com API error: ${data.status || "No results"} ${data.error || ""}`);
7190
+ const data = (await response.json());
7191
+ if ("message" in data) {
7192
+ throw new Error(`Massive.com API error: ${data.message}`);
7174
7193
  }
7175
- const { p: price, s: vol, t: timestamp } = data.results;
7194
+ if (!data.results ||
7195
+ !Array.isArray(data.results) ||
7196
+ data.results.length === 0) {
7197
+ throw new Error(`Massive.com API error: No trade results for ${symbol}`);
7198
+ }
7199
+ const latestTrade = data.results[0];
7200
+ const price = latestTrade.price;
7201
+ const vol = latestTrade.size;
7202
+ const timestamp = latestTrade.sip_timestamp;
7176
7203
  if (typeof price !== "number" ||
7177
7204
  typeof vol !== "number" ||
7178
7205
  typeof timestamp !== "number") {
@@ -7205,6 +7232,77 @@ const fetchLastTrade = async (symbol, options) => {
7205
7232
  }
7206
7233
  });
7207
7234
  };
7235
+ /**
7236
+ * Fetches the latest NBBO quote for a given stock ticker using the Massive v3 API.
7237
+ * Returns processed spread information including bid, ask, spread, and mid price.
7238
+ *
7239
+ * @param symbol - The stock ticker symbol.
7240
+ * @param options - Optional parameters including API key override.
7241
+ * @returns Processed quote data with spread metrics.
7242
+ */
7243
+ const fetchLastQuote = async (symbol, options) => {
7244
+ if (!options?.apiKey && !MASSIVE_API_KEY) {
7245
+ throw new Error("Massive API key is missing");
7246
+ }
7247
+ const apiKey = options?.apiKey || MASSIVE_API_KEY;
7248
+ validateMassiveApiKey$1(apiKey);
7249
+ const baseUrl = `https://api.massive.com/v3/quotes/${encodeURIComponent(symbol)}`;
7250
+ const params = new URLSearchParams({
7251
+ apiKey,
7252
+ limit: "1",
7253
+ sort: "timestamp",
7254
+ order: "desc",
7255
+ });
7256
+ return massiveLimit(async () => {
7257
+ try {
7258
+ const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
7259
+ const data = (await response.json());
7260
+ if ("message" in data) {
7261
+ throw new Error(`Massive.com API error: ${data.message}`);
7262
+ }
7263
+ const results = data.results;
7264
+ if (!Array.isArray(results) || results.length === 0) {
7265
+ throw new Error(`Massive.com API error: No quote results for ${symbol}`);
7266
+ }
7267
+ const quote = results[0];
7268
+ if (typeof quote.bid_price !== "number" ||
7269
+ typeof quote.ask_price !== "number") {
7270
+ throw new Error("Invalid quote data received from Massive.com API");
7271
+ }
7272
+ const midPrice = (quote.bid_price + quote.ask_price) / 2;
7273
+ const spread = quote.ask_price - quote.bid_price;
7274
+ return {
7275
+ bid: quote.bid_price,
7276
+ ask: quote.ask_price,
7277
+ spread,
7278
+ spreadPercent: midPrice > 0 ? (spread / midPrice) * 100 : 0,
7279
+ midPrice,
7280
+ bidSize: quote.bid_size,
7281
+ askSize: quote.ask_size,
7282
+ time: new Date(Math.floor(quote.sip_timestamp / 1000000)),
7283
+ };
7284
+ }
7285
+ catch (error) {
7286
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
7287
+ const contextualMessage = `Error fetching last quote for ${symbol}`;
7288
+ getLogger().error(`${contextualMessage}: ${errorMessage}`, {
7289
+ symbol,
7290
+ errorType: error instanceof Error && error.message.includes("AUTH_ERROR")
7291
+ ? "AUTH_ERROR"
7292
+ : error instanceof Error && error.message.includes("RATE_LIMIT")
7293
+ ? "RATE_LIMIT"
7294
+ : error instanceof Error &&
7295
+ error.message.includes("NETWORK_ERROR")
7296
+ ? "NETWORK_ERROR"
7297
+ : "UNKNOWN",
7298
+ url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
7299
+ source: "MassiveAPI.fetchLastQuote",
7300
+ timestamp: new Date().toISOString(),
7301
+ });
7302
+ throw new Error(`${contextualMessage}: ${errorMessage}`);
7303
+ }
7304
+ });
7305
+ };
7208
7306
  // use Massive for all price data fetching
7209
7307
  /**
7210
7308
  * Fetches price data for a given stock ticker.
@@ -66228,22 +66326,25 @@ const MassiveTradesResponseSchema = objectType({
66228
66326
  results: arrayType(MassiveTradeSchema),
66229
66327
  });
66230
66328
  // ===== Last Trade Schemas =====
66231
- /** Schema for Massive last trade response */
66329
+ /** Schema for Massive last trade response (v3 format - returns array of trades) */
66232
66330
  const MassiveLastTradeResponseSchema = objectType({
66233
66331
  status: stringType(),
66234
66332
  request_id: stringType(),
66235
- results: objectType({
66236
- T: stringType(),
66237
- p: numberType(),
66238
- s: numberType(),
66239
- t: numberType(),
66240
- c: arrayType(numberType()).optional(),
66241
- e: numberType().optional(),
66242
- i: stringType().optional(),
66243
- q: numberType().optional(),
66244
- x: numberType().optional(),
66245
- z: numberType().optional(),
66246
- }),
66333
+ results: arrayType(objectType({
66334
+ conditions: arrayType(numberType()).optional(),
66335
+ correction: numberType().optional(),
66336
+ exchange: numberType(),
66337
+ id: stringType(),
66338
+ participant_timestamp: numberType(),
66339
+ price: numberType(),
66340
+ sequence_number: numberType(),
66341
+ sip_timestamp: numberType(),
66342
+ size: numberType(),
66343
+ tape: numberType().optional(),
66344
+ trf_id: numberType().optional(),
66345
+ trf_timestamp: numberType().optional(),
66346
+ }))
66347
+ .min(1),
66247
66348
  });
66248
66349
  // ===== Aggregates (Bars) Schemas =====
66249
66350
  /** Schema for Massive aggregates (bars) response */
@@ -67419,6 +67520,7 @@ const adaptic = {
67419
67520
  fetchTickerInfo: fetchTickerInfo,
67420
67521
  fetchGroupedDaily: fetchGroupedDaily,
67421
67522
  fetchLastTrade: fetchLastTrade,
67523
+ fetchLastQuote: fetchLastQuote,
67422
67524
  fetchTrades: fetchTrades,
67423
67525
  fetchPrices: fetchPrices,
67424
67526
  analyseMassivePriceData: analyseMassivePriceData,