@discomedia/utils 1.0.77 → 1.0.78
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-frontend.cjs +2024 -15270
- package/dist/index-frontend.cjs.map +7 -1
- package/dist/index-frontend.mjs +1985 -15260
- package/dist/index-frontend.mjs.map +7 -1
- package/dist/index.cjs +5368 -24387
- package/dist/index.cjs.map +7 -1
- package/dist/index.mjs +5326 -24374
- package/dist/index.mjs.map +7 -1
- package/dist/package.json +10 -11
- package/dist/test.mjs +1977 -0
- package/dist/test.mjs.map +7 -0
- package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/index-frontend.d.ts +2 -1
- package/dist/types/index-frontend.d.ts.map +1 -1
- package/dist/types/index.d.ts +7 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/llm-config.d.ts +4 -2
- package/dist/types/llm-config.d.ts.map +1 -1
- package/dist/types/llm-deepseek.d.ts.map +1 -1
- package/dist/types/llm-images.d.ts.map +1 -1
- package/dist/types/llm-openai.d.ts +7 -4
- package/dist/types/llm-openai.d.ts.map +1 -1
- package/dist/types/llm-openrouter.d.ts.map +1 -1
- package/dist/types/market-time.d.ts +1 -1
- package/dist/types/market-time.d.ts.map +1 -1
- package/dist/types/misc-utils.d.ts +1 -1
- package/dist/types/misc-utils.d.ts.map +1 -1
- package/dist/types/performance-timer.d.ts.map +1 -1
- package/dist/types/types/llm-types.d.ts +8 -5
- package/dist/types/types/llm-types.d.ts.map +1 -1
- package/package.json +10 -11
- package/dist/test.js +0 -17500
- package/dist/test.js.map +0 -1
- package/dist/types-frontend/alpaca-market-data-api.d.ts +0 -451
- package/dist/types-frontend/alpaca-market-data-api.d.ts.map +0 -1
- package/dist/types-frontend/alpaca-trading-api.d.ts +0 -407
- package/dist/types-frontend/alpaca-trading-api.d.ts.map +0 -1
- package/dist/types-frontend/format-tools.d.ts +0 -46
- package/dist/types-frontend/format-tools.d.ts.map +0 -1
- package/dist/types-frontend/index-frontend.d.ts +0 -18
- package/dist/types-frontend/index-frontend.d.ts.map +0 -1
- package/dist/types-frontend/index.d.ts +0 -59
- package/dist/types-frontend/index.d.ts.map +0 -1
- package/dist/types-frontend/json-tools.d.ts +0 -31
- package/dist/types-frontend/json-tools.d.ts.map +0 -1
- package/dist/types-frontend/llm-config.d.ts +0 -51
- package/dist/types-frontend/llm-config.d.ts.map +0 -1
- package/dist/types-frontend/llm-deepseek.d.ts +0 -12
- package/dist/types-frontend/llm-deepseek.d.ts.map +0 -1
- package/dist/types-frontend/llm-images.d.ts +0 -52
- package/dist/types-frontend/llm-images.d.ts.map +0 -1
- package/dist/types-frontend/llm-openai.d.ts +0 -79
- package/dist/types-frontend/llm-openai.d.ts.map +0 -1
- package/dist/types-frontend/llm-openrouter.d.ts +0 -28
- package/dist/types-frontend/llm-openrouter.d.ts.map +0 -1
- package/dist/types-frontend/llm-utils.d.ts +0 -16
- package/dist/types-frontend/llm-utils.d.ts.map +0 -1
- package/dist/types-frontend/logging.d.ts +0 -12
- package/dist/types-frontend/logging.d.ts.map +0 -1
- package/dist/types-frontend/market-hours.d.ts +0 -24
- package/dist/types-frontend/market-hours.d.ts.map +0 -1
- package/dist/types-frontend/market-time.d.ts +0 -270
- package/dist/types-frontend/market-time.d.ts.map +0 -1
- package/dist/types-frontend/misc-utils.d.ts +0 -43
- package/dist/types-frontend/misc-utils.d.ts.map +0 -1
- package/dist/types-frontend/performance-timer.d.ts +0 -73
- package/dist/types-frontend/performance-timer.d.ts.map +0 -1
- package/dist/types-frontend/test.d.ts +0 -2
- package/dist/types-frontend/test.d.ts.map +0 -1
- package/dist/types-frontend/testing/market-time-refactor-test.d.ts +0 -1
- package/dist/types-frontend/testing/market-time-refactor-test.d.ts.map +0 -1
- package/dist/types-frontend/types/alpaca-crypto-pairs.d.ts +0 -11
- package/dist/types-frontend/types/alpaca-crypto-pairs.d.ts.map +0 -1
- package/dist/types-frontend/types/alpaca-types.d.ts +0 -1181
- package/dist/types-frontend/types/alpaca-types.d.ts.map +0 -1
- package/dist/types-frontend/types/index.d.ts +0 -6
- package/dist/types-frontend/types/index.d.ts.map +0 -1
- package/dist/types-frontend/types/llm-types.d.ts +0 -174
- package/dist/types-frontend/types/llm-types.d.ts.map +0 -1
- package/dist/types-frontend/types/logging-types.d.ts +0 -10
- package/dist/types-frontend/types/logging-types.d.ts.map +0 -1
- package/dist/types-frontend/types/market-time-types.d.ts +0 -64
- package/dist/types-frontend/types/market-time-types.d.ts.map +0 -1
- package/dist/types-frontend/types/ta-types.d.ts +0 -89
- package/dist/types-frontend/types/ta-types.d.ts.map +0 -1
package/dist/test.mjs
ADDED
|
@@ -0,0 +1,1977 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __esm = (fn, res, err) => function __init() {
|
|
3
|
+
if (err) throw err[0];
|
|
4
|
+
try {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw err = [e], e;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/logging.ts
|
|
12
|
+
function log(message, options = { source: "App", type: "info" }) {
|
|
13
|
+
const date = /* @__PURE__ */ new Date();
|
|
14
|
+
const timestamp = date.toLocaleString("en-US", { timeZone: "America/New_York" });
|
|
15
|
+
const account = options?.account;
|
|
16
|
+
const symbol = options?.symbol;
|
|
17
|
+
const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ""}${account ? ` [${account}] ` : ""}${symbol ? ` [${symbol}] ` : ""}${message}`;
|
|
18
|
+
if (options?.type === "error") {
|
|
19
|
+
console.error(logMessage);
|
|
20
|
+
} else if (options?.type === "warn") {
|
|
21
|
+
console.warn(logMessage);
|
|
22
|
+
} else {
|
|
23
|
+
console.log(logMessage);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var init_logging = __esm({
|
|
27
|
+
"src/logging.ts"() {
|
|
28
|
+
"use strict";
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/market-hours.ts
|
|
33
|
+
var marketHolidays, marketEarlyCloses;
|
|
34
|
+
var init_market_hours = __esm({
|
|
35
|
+
"src/market-hours.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
marketHolidays = {
|
|
38
|
+
2024: {
|
|
39
|
+
"New Year's Day": { date: "2024-01-01" },
|
|
40
|
+
"Martin Luther King, Jr. Day": { date: "2024-01-15" },
|
|
41
|
+
"Washington's Birthday": { date: "2024-02-19" },
|
|
42
|
+
"Good Friday": { date: "2024-03-29" },
|
|
43
|
+
"Memorial Day": { date: "2024-05-27" },
|
|
44
|
+
"Juneteenth National Independence Day": { date: "2024-06-19" },
|
|
45
|
+
"Independence Day": { date: "2024-07-04" },
|
|
46
|
+
"Labor Day": { date: "2024-09-02" },
|
|
47
|
+
"Thanksgiving Day": { date: "2024-11-28" },
|
|
48
|
+
"Christmas Day": { date: "2024-12-25" }
|
|
49
|
+
},
|
|
50
|
+
2025: {
|
|
51
|
+
"New Year's Day": { date: "2025-01-01" },
|
|
52
|
+
"Jimmy Carter Memorial Day": { date: "2025-01-09" },
|
|
53
|
+
"Martin Luther King, Jr. Day": { date: "2025-01-20" },
|
|
54
|
+
"Washington's Birthday": { date: "2025-02-17" },
|
|
55
|
+
"Good Friday": { date: "2025-04-18" },
|
|
56
|
+
"Memorial Day": { date: "2025-05-26" },
|
|
57
|
+
"Juneteenth National Independence Day": { date: "2025-06-19" },
|
|
58
|
+
"Independence Day": { date: "2025-07-04" },
|
|
59
|
+
"Labor Day": { date: "2025-09-01" },
|
|
60
|
+
"Thanksgiving Day": { date: "2025-11-27" },
|
|
61
|
+
"Christmas Day": { date: "2025-12-25" }
|
|
62
|
+
},
|
|
63
|
+
2026: {
|
|
64
|
+
"New Year's Day": { date: "2026-01-01" },
|
|
65
|
+
"Martin Luther King, Jr. Day": { date: "2026-01-19" },
|
|
66
|
+
"Washington's Birthday": { date: "2026-02-16" },
|
|
67
|
+
"Good Friday": { date: "2026-04-03" },
|
|
68
|
+
"Memorial Day": { date: "2026-05-25" },
|
|
69
|
+
"Juneteenth National Independence Day": { date: "2026-06-19" },
|
|
70
|
+
"Independence Day": { date: "2026-07-03" },
|
|
71
|
+
"Labor Day": { date: "2026-09-07" },
|
|
72
|
+
"Thanksgiving Day": { date: "2026-11-26" },
|
|
73
|
+
"Christmas Day": { date: "2026-12-25" }
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
marketEarlyCloses = {
|
|
77
|
+
2024: {
|
|
78
|
+
"2024-07-03": {
|
|
79
|
+
date: "2024-07-03",
|
|
80
|
+
time: "13:00",
|
|
81
|
+
optionsTime: "13:15",
|
|
82
|
+
notes: "Market closes early on Wednesday, July 3, 2024 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
83
|
+
},
|
|
84
|
+
"2024-11-29": {
|
|
85
|
+
date: "2024-11-29",
|
|
86
|
+
time: "13:00",
|
|
87
|
+
optionsTime: "13:15",
|
|
88
|
+
notes: "Market closes early on Friday, November 29, 2024 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
89
|
+
},
|
|
90
|
+
"2024-12-24": {
|
|
91
|
+
date: "2024-12-24",
|
|
92
|
+
time: "13:00",
|
|
93
|
+
optionsTime: "13:15",
|
|
94
|
+
notes: "Market closes early on Tuesday, December 24, 2024 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
2025: {
|
|
98
|
+
"2025-07-03": {
|
|
99
|
+
date: "2025-07-03",
|
|
100
|
+
time: "13:00",
|
|
101
|
+
optionsTime: "13:15",
|
|
102
|
+
notes: "Market closes early on Thursday, July 3, 2025 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
103
|
+
},
|
|
104
|
+
"2025-11-28": {
|
|
105
|
+
date: "2025-11-28",
|
|
106
|
+
time: "13:00",
|
|
107
|
+
optionsTime: "13:15",
|
|
108
|
+
notes: "Market closes early on Friday, November 28, 2025 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
109
|
+
},
|
|
110
|
+
"2025-12-24": {
|
|
111
|
+
date: "2025-12-24",
|
|
112
|
+
time: "13:00",
|
|
113
|
+
optionsTime: "13:15",
|
|
114
|
+
notes: "Market closes early on Wednesday, December 24, 2025 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
2026: {
|
|
118
|
+
"2026-07-02": {
|
|
119
|
+
date: "2026-07-02",
|
|
120
|
+
time: "13:00",
|
|
121
|
+
optionsTime: "13:15",
|
|
122
|
+
notes: "Independence Day observed, market closes early at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
123
|
+
},
|
|
124
|
+
"2026-11-27": {
|
|
125
|
+
date: "2026-11-27",
|
|
126
|
+
time: "13:00",
|
|
127
|
+
optionsTime: "13:15",
|
|
128
|
+
notes: "Market closes early on Friday, November 27, 2026 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
129
|
+
},
|
|
130
|
+
"2026-12-24": {
|
|
131
|
+
date: "2026-12-24",
|
|
132
|
+
time: "13:00",
|
|
133
|
+
optionsTime: "13:15",
|
|
134
|
+
notes: "Market closes early on Thursday, December 24, 2026 at 1:00 p.m. (1:15 p.m. for eligible options). NYSE American Equities, NYSE Arca Equities, NYSE Chicago, and NYSE National late trading sessions will close at 5:00 p.m. Eastern Time."
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// src/market-time.ts
|
|
142
|
+
function getNYOffset(date) {
|
|
143
|
+
const year = date.getUTCFullYear();
|
|
144
|
+
const dstStart = getNthWeekdayOfMonth(year, 3, 0, 2);
|
|
145
|
+
const dstEnd = getNthWeekdayOfMonth(year, 11, 0, 1);
|
|
146
|
+
const utcTime = date.getTime();
|
|
147
|
+
if (utcTime >= dstStart.getTime() && utcTime < dstEnd.getTime()) {
|
|
148
|
+
return MARKET_CONFIG.UTC_OFFSET_DST;
|
|
149
|
+
}
|
|
150
|
+
return MARKET_CONFIG.UTC_OFFSET_STANDARD;
|
|
151
|
+
}
|
|
152
|
+
function getNthWeekdayOfMonth(year, month, weekday, n) {
|
|
153
|
+
const firstDay = new Date(Date.UTC(year, month - 1, 1));
|
|
154
|
+
let count = 0;
|
|
155
|
+
for (let d = 1; d <= 31; d++) {
|
|
156
|
+
const date = new Date(Date.UTC(year, month - 1, d));
|
|
157
|
+
if (date.getUTCMonth() !== month - 1) break;
|
|
158
|
+
if (date.getUTCDay() === weekday) {
|
|
159
|
+
count++;
|
|
160
|
+
if (count === n) return date;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return new Date(Date.UTC(year, month - 1, 28));
|
|
164
|
+
}
|
|
165
|
+
function toNYTime(date) {
|
|
166
|
+
const offset = getNYOffset(date);
|
|
167
|
+
const utcMillis = date.getTime();
|
|
168
|
+
const nyMillis = utcMillis + offset * 60 * 60 * 1e3;
|
|
169
|
+
return new Date(nyMillis);
|
|
170
|
+
}
|
|
171
|
+
function fromNYTime(date) {
|
|
172
|
+
const offset = getNYOffset(date);
|
|
173
|
+
const nyMillis = date.getTime();
|
|
174
|
+
const utcMillis = nyMillis - offset * 60 * 60 * 1e3;
|
|
175
|
+
return new Date(utcMillis);
|
|
176
|
+
}
|
|
177
|
+
function getLastFullTradingDateImpl(currentDate = /* @__PURE__ */ new Date()) {
|
|
178
|
+
const calendar = new MarketCalendar();
|
|
179
|
+
const nyDate = toNYTime(currentDate);
|
|
180
|
+
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
181
|
+
const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
182
|
+
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
183
|
+
if (calendar.isEarlyCloseDay(currentDate)) {
|
|
184
|
+
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
185
|
+
}
|
|
186
|
+
if (!calendar.isMarketDay(currentDate) || minutes < marketOpenMinutes || minutes >= marketOpenMinutes && minutes < marketCloseMinutes) {
|
|
187
|
+
const prevMarketDay = calendar.getPreviousMarketDay(currentDate);
|
|
188
|
+
let prevCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
189
|
+
if (calendar.isEarlyCloseDay(prevMarketDay)) {
|
|
190
|
+
prevCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
191
|
+
}
|
|
192
|
+
const prevNYDate = toNYTime(prevMarketDay);
|
|
193
|
+
const year2 = prevNYDate.getUTCFullYear();
|
|
194
|
+
const month2 = prevNYDate.getUTCMonth();
|
|
195
|
+
const day2 = prevNYDate.getUTCDate();
|
|
196
|
+
const closeHour2 = Math.floor(prevCloseMinutes / 60);
|
|
197
|
+
const closeMinute2 = prevCloseMinutes % 60;
|
|
198
|
+
return fromNYTime(new Date(Date.UTC(year2, month2, day2, closeHour2, closeMinute2, 0, 0)));
|
|
199
|
+
}
|
|
200
|
+
const year = nyDate.getUTCFullYear();
|
|
201
|
+
const month = nyDate.getUTCMonth();
|
|
202
|
+
const day = nyDate.getUTCDate();
|
|
203
|
+
const closeHour = Math.floor(marketCloseMinutes / 60);
|
|
204
|
+
const closeMinute = marketCloseMinutes % 60;
|
|
205
|
+
return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
|
|
206
|
+
}
|
|
207
|
+
function getLastFullTradingDate(currentDate = /* @__PURE__ */ new Date()) {
|
|
208
|
+
return getLastFullTradingDateImpl(currentDate);
|
|
209
|
+
}
|
|
210
|
+
var MARKET_CONFIG, MarketCalendar, MARKET_TIMES;
|
|
211
|
+
var init_market_time = __esm({
|
|
212
|
+
"src/market-time.ts"() {
|
|
213
|
+
"use strict";
|
|
214
|
+
init_logging();
|
|
215
|
+
init_market_hours();
|
|
216
|
+
MARKET_CONFIG = {
|
|
217
|
+
TIMEZONE: "America/New_York",
|
|
218
|
+
UTC_OFFSET_STANDARD: -5,
|
|
219
|
+
// EST
|
|
220
|
+
UTC_OFFSET_DST: -4,
|
|
221
|
+
// EDT
|
|
222
|
+
TIMES: {
|
|
223
|
+
EXTENDED_START: { hour: 4, minute: 0 },
|
|
224
|
+
MARKET_OPEN: { hour: 9, minute: 30 },
|
|
225
|
+
EARLY_MARKET_END: { hour: 10, minute: 0 },
|
|
226
|
+
MARKET_CLOSE: { hour: 16, minute: 0 },
|
|
227
|
+
EARLY_CLOSE: { hour: 13, minute: 0 },
|
|
228
|
+
EXTENDED_END: { hour: 20, minute: 0 },
|
|
229
|
+
EARLY_EXTENDED_END: { hour: 17, minute: 0 }
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
MarketCalendar = class {
|
|
233
|
+
/**
|
|
234
|
+
* Checks if a date is a weekend in NY time.
|
|
235
|
+
* @param date - Date object
|
|
236
|
+
* @returns true if weekend, false otherwise
|
|
237
|
+
*/
|
|
238
|
+
isWeekend(date) {
|
|
239
|
+
const day = toNYTime(date).getUTCDay();
|
|
240
|
+
return day === 0 || day === 6;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Checks if a date is a market holiday in NY time.
|
|
244
|
+
* @param date - Date object
|
|
245
|
+
* @returns true if holiday, false otherwise
|
|
246
|
+
*/
|
|
247
|
+
isHoliday(date) {
|
|
248
|
+
const nyDate = toNYTime(date);
|
|
249
|
+
const year = nyDate.getUTCFullYear();
|
|
250
|
+
const month = nyDate.getUTCMonth() + 1;
|
|
251
|
+
const day = nyDate.getUTCDate();
|
|
252
|
+
const formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
253
|
+
const yearHolidays = marketHolidays[year];
|
|
254
|
+
if (!yearHolidays) return false;
|
|
255
|
+
return Object.values(yearHolidays).some((holiday) => holiday.date === formattedDate);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Checks if a date is an early close day in NY time.
|
|
259
|
+
* @param date - Date object
|
|
260
|
+
* @returns true if early close, false otherwise
|
|
261
|
+
*/
|
|
262
|
+
isEarlyCloseDay(date) {
|
|
263
|
+
const nyDate = toNYTime(date);
|
|
264
|
+
const year = nyDate.getUTCFullYear();
|
|
265
|
+
const month = nyDate.getUTCMonth() + 1;
|
|
266
|
+
const day = nyDate.getUTCDate();
|
|
267
|
+
const formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
268
|
+
const yearEarlyCloses = marketEarlyCloses[year];
|
|
269
|
+
return yearEarlyCloses && yearEarlyCloses[formattedDate] !== void 0;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Gets the early close time (in minutes from midnight) for a given date.
|
|
273
|
+
* @param date - Date object
|
|
274
|
+
* @returns minutes from midnight or null
|
|
275
|
+
*/
|
|
276
|
+
getEarlyCloseTime(date) {
|
|
277
|
+
const nyDate = toNYTime(date);
|
|
278
|
+
const year = nyDate.getUTCFullYear();
|
|
279
|
+
const month = nyDate.getUTCMonth() + 1;
|
|
280
|
+
const day = nyDate.getUTCDate();
|
|
281
|
+
const formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
282
|
+
const yearEarlyCloses = marketEarlyCloses[year];
|
|
283
|
+
if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
|
|
284
|
+
const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(":").map(Number);
|
|
285
|
+
return hours * 60 + minutes;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Checks if a date is a market day (not weekend or holiday).
|
|
291
|
+
* @param date - Date object
|
|
292
|
+
* @returns true if market day, false otherwise
|
|
293
|
+
*/
|
|
294
|
+
isMarketDay(date) {
|
|
295
|
+
return !this.isWeekend(date) && !this.isHoliday(date);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Gets the next market day after the given date.
|
|
299
|
+
* @param date - Date object
|
|
300
|
+
* @returns Date object for next market day
|
|
301
|
+
*/
|
|
302
|
+
getNextMarketDay(date) {
|
|
303
|
+
let nextDay = new Date(date.getTime() + 24 * 60 * 60 * 1e3);
|
|
304
|
+
while (!this.isMarketDay(nextDay)) {
|
|
305
|
+
nextDay = new Date(nextDay.getTime() + 24 * 60 * 60 * 1e3);
|
|
306
|
+
}
|
|
307
|
+
return nextDay;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Gets the previous market day before the given date.
|
|
311
|
+
* @param date - Date object
|
|
312
|
+
* @returns Date object for previous market day
|
|
313
|
+
*/
|
|
314
|
+
getPreviousMarketDay(date) {
|
|
315
|
+
let prevDay = new Date(date.getTime() - 24 * 60 * 60 * 1e3);
|
|
316
|
+
while (!this.isMarketDay(prevDay)) {
|
|
317
|
+
prevDay = new Date(prevDay.getTime() - 24 * 60 * 60 * 1e3);
|
|
318
|
+
}
|
|
319
|
+
return prevDay;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
MARKET_TIMES = {
|
|
323
|
+
TIMEZONE: MARKET_CONFIG.TIMEZONE,
|
|
324
|
+
PRE: {
|
|
325
|
+
START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
|
|
326
|
+
END: { HOUR: 9, MINUTE: 30, MINUTES: 570 }
|
|
327
|
+
},
|
|
328
|
+
EARLY_MORNING: {
|
|
329
|
+
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
330
|
+
END: { HOUR: 10, MINUTE: 0, MINUTES: 600 }
|
|
331
|
+
},
|
|
332
|
+
EARLY_CLOSE_BEFORE_HOLIDAY: {
|
|
333
|
+
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
334
|
+
END: { HOUR: 13, MINUTE: 0, MINUTES: 780 }
|
|
335
|
+
},
|
|
336
|
+
EARLY_EXTENDED_BEFORE_HOLIDAY: {
|
|
337
|
+
START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
|
|
338
|
+
END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 }
|
|
339
|
+
},
|
|
340
|
+
REGULAR: {
|
|
341
|
+
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
342
|
+
END: { HOUR: 16, MINUTE: 0, MINUTES: 960 }
|
|
343
|
+
},
|
|
344
|
+
EXTENDED: {
|
|
345
|
+
START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
|
|
346
|
+
END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 }
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// src/alpaca-market-data-api.ts
|
|
353
|
+
import "dotenv/config";
|
|
354
|
+
import { EventEmitter } from "events";
|
|
355
|
+
import WebSocket from "ws";
|
|
356
|
+
var DEBUG_LOGGING, log2, DEFAULT_ADJUSTMENT, DEFAULT_FEED, DEFAULT_CURRENCY, AlpacaMarketDataAPI, marketDataAPI;
|
|
357
|
+
var init_alpaca_market_data_api = __esm({
|
|
358
|
+
"src/alpaca-market-data-api.ts"() {
|
|
359
|
+
"use strict";
|
|
360
|
+
init_logging();
|
|
361
|
+
init_market_time();
|
|
362
|
+
DEBUG_LOGGING = process.env["DEBUG"] === "true" || false;
|
|
363
|
+
log2 = (message, options = { type: "info" }) => {
|
|
364
|
+
if (!DEBUG_LOGGING && options.type === "debug") {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
log(message, { ...options, source: "AlpacaMarketDataAPI" });
|
|
368
|
+
};
|
|
369
|
+
DEFAULT_ADJUSTMENT = "all";
|
|
370
|
+
DEFAULT_FEED = "sip";
|
|
371
|
+
DEFAULT_CURRENCY = "USD";
|
|
372
|
+
AlpacaMarketDataAPI = class _AlpacaMarketDataAPI extends EventEmitter {
|
|
373
|
+
static instance;
|
|
374
|
+
headers;
|
|
375
|
+
dataURL;
|
|
376
|
+
apiURL;
|
|
377
|
+
v1beta1url;
|
|
378
|
+
v1beta3url;
|
|
379
|
+
stockStreamUrl = "wss://stream.data.alpaca.markets/v2/sip";
|
|
380
|
+
// production values
|
|
381
|
+
optionStreamUrl = "wss://stream.data.alpaca.markets/v1beta3/options";
|
|
382
|
+
// production values
|
|
383
|
+
stockWs = null;
|
|
384
|
+
optionWs = null;
|
|
385
|
+
stockSubscriptions = { trades: [], quotes: [], bars: [] };
|
|
386
|
+
optionSubscriptions = { trades: [], quotes: [], bars: [] };
|
|
387
|
+
autoReconnect = true;
|
|
388
|
+
stockReconnectAttempts = 0;
|
|
389
|
+
optionReconnectAttempts = 0;
|
|
390
|
+
stockReconnectTimeout = null;
|
|
391
|
+
optionReconnectTimeout = null;
|
|
392
|
+
setMode(mode = "production") {
|
|
393
|
+
if (mode === "sandbox") {
|
|
394
|
+
this.stockStreamUrl = "wss://stream.data.sandbox.alpaca.markets/v2/sip";
|
|
395
|
+
this.optionStreamUrl = "wss://stream.data.sandbox.alpaca.markets/v1beta3/options";
|
|
396
|
+
} else if (mode === "test") {
|
|
397
|
+
this.stockStreamUrl = "wss://stream.data.alpaca.markets/v2/test";
|
|
398
|
+
this.optionStreamUrl = "wss://stream.data.alpaca.markets/v1beta3/options";
|
|
399
|
+
} else {
|
|
400
|
+
this.stockStreamUrl = "wss://stream.data.alpaca.markets/v2/sip";
|
|
401
|
+
this.optionStreamUrl = "wss://stream.data.alpaca.markets/v1beta3/options";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
getMode() {
|
|
405
|
+
if (this.stockStreamUrl.includes("sandbox")) {
|
|
406
|
+
return "sandbox";
|
|
407
|
+
} else if (this.stockStreamUrl.includes("test")) {
|
|
408
|
+
return "test";
|
|
409
|
+
} else {
|
|
410
|
+
return "production";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
setAutoReconnect(enabled) {
|
|
414
|
+
this.autoReconnect = enabled;
|
|
415
|
+
log2(`Auto-reconnect ${enabled ? "enabled" : "disabled"}`);
|
|
416
|
+
}
|
|
417
|
+
getAutoReconnect() {
|
|
418
|
+
return this.autoReconnect;
|
|
419
|
+
}
|
|
420
|
+
constructor() {
|
|
421
|
+
super();
|
|
422
|
+
this.dataURL = "https://data.alpaca.markets/v2";
|
|
423
|
+
this.apiURL = process.env.ALPACA_ACCOUNT_TYPE === "PAPER" ? "https://paper-api.alpaca.markets/v2" : "https://api.alpaca.markets/v2";
|
|
424
|
+
this.v1beta1url = "https://data.alpaca.markets/v1beta1";
|
|
425
|
+
this.v1beta3url = "https://data.alpaca.markets/v1beta3";
|
|
426
|
+
this.setMode("production");
|
|
427
|
+
this.headers = {
|
|
428
|
+
"APCA-API-KEY-ID": process.env.ALPACA_API_KEY,
|
|
429
|
+
"APCA-API-SECRET-KEY": process.env.ALPACA_SECRET_KEY,
|
|
430
|
+
"Content-Type": "application/json"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
static getInstance() {
|
|
434
|
+
if (!_AlpacaMarketDataAPI.instance) {
|
|
435
|
+
_AlpacaMarketDataAPI.instance = new _AlpacaMarketDataAPI();
|
|
436
|
+
}
|
|
437
|
+
return _AlpacaMarketDataAPI.instance;
|
|
438
|
+
}
|
|
439
|
+
getReconnectDelay(attempt) {
|
|
440
|
+
const delays = [0, 5e3, 1e4, 15e3, 6e4];
|
|
441
|
+
return attempt < delays.length ? delays[attempt] : 6e4;
|
|
442
|
+
}
|
|
443
|
+
scheduleReconnect(streamType) {
|
|
444
|
+
if (!this.autoReconnect) {
|
|
445
|
+
log2(`Auto-reconnect disabled, not reconnecting ${streamType} stream`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const attempts = streamType === "stock" ? this.stockReconnectAttempts : this.optionReconnectAttempts;
|
|
449
|
+
const delay = this.getReconnectDelay(attempts);
|
|
450
|
+
log2(`Scheduling ${streamType} stream reconnect (attempt ${attempts + 1}) in ${delay}ms`);
|
|
451
|
+
const timeout = setTimeout(() => {
|
|
452
|
+
log2(`Attempting to reconnect ${streamType} stream (attempt ${attempts + 1})`);
|
|
453
|
+
if (streamType === "stock") {
|
|
454
|
+
this.stockReconnectAttempts++;
|
|
455
|
+
} else {
|
|
456
|
+
this.optionReconnectAttempts++;
|
|
457
|
+
}
|
|
458
|
+
this.connect(streamType);
|
|
459
|
+
}, delay);
|
|
460
|
+
if (streamType === "stock") {
|
|
461
|
+
this.stockReconnectTimeout = timeout;
|
|
462
|
+
} else {
|
|
463
|
+
this.optionReconnectTimeout = timeout;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
on(event, listener) {
|
|
467
|
+
return super.on(event, listener);
|
|
468
|
+
}
|
|
469
|
+
emit(event, ...args) {
|
|
470
|
+
return super.emit(event, ...args);
|
|
471
|
+
}
|
|
472
|
+
connect(streamType) {
|
|
473
|
+
const url = streamType === "stock" ? this.stockStreamUrl : this.optionStreamUrl;
|
|
474
|
+
const ws = new WebSocket(url);
|
|
475
|
+
if (streamType === "stock") {
|
|
476
|
+
this.stockWs = ws;
|
|
477
|
+
} else {
|
|
478
|
+
this.optionWs = ws;
|
|
479
|
+
}
|
|
480
|
+
ws.on("open", () => {
|
|
481
|
+
log2(`${streamType} stream connected`);
|
|
482
|
+
if (streamType === "stock") {
|
|
483
|
+
this.stockReconnectAttempts = 0;
|
|
484
|
+
if (this.stockReconnectTimeout) {
|
|
485
|
+
clearTimeout(this.stockReconnectTimeout);
|
|
486
|
+
this.stockReconnectTimeout = null;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
this.optionReconnectAttempts = 0;
|
|
490
|
+
if (this.optionReconnectTimeout) {
|
|
491
|
+
clearTimeout(this.optionReconnectTimeout);
|
|
492
|
+
this.optionReconnectTimeout = null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const authMessage = {
|
|
496
|
+
action: "auth",
|
|
497
|
+
key: process.env.ALPACA_API_KEY,
|
|
498
|
+
secret: process.env.ALPACA_SECRET_KEY
|
|
499
|
+
};
|
|
500
|
+
ws.send(JSON.stringify(authMessage));
|
|
501
|
+
});
|
|
502
|
+
ws.on("message", (data) => {
|
|
503
|
+
const messages = JSON.parse(data.toString());
|
|
504
|
+
for (const message of messages) {
|
|
505
|
+
if (message.T === "success" && message.msg === "authenticated") {
|
|
506
|
+
log2(`${streamType} stream authenticated`);
|
|
507
|
+
this.sendSubscription(streamType);
|
|
508
|
+
} else if (message.T === "error") {
|
|
509
|
+
log2(`${streamType} stream error: ${message.msg}`, { type: "error" });
|
|
510
|
+
} else if (message.S) {
|
|
511
|
+
super.emit(`${streamType}-${message.T}`, message);
|
|
512
|
+
super.emit(`${streamType}-data`, message);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
ws.on("close", (code, reason) => {
|
|
517
|
+
const reasonStr = reason.toString() || "No reason provided";
|
|
518
|
+
const codeDesc = this.getCloseCodeDescription(code);
|
|
519
|
+
log2(`${streamType} stream disconnected - Code: ${code} (${codeDesc}), Reason: ${reasonStr}`, { type: "warn" });
|
|
520
|
+
if (streamType === "stock") {
|
|
521
|
+
this.stockWs = null;
|
|
522
|
+
} else {
|
|
523
|
+
this.optionWs = null;
|
|
524
|
+
}
|
|
525
|
+
this.scheduleReconnect(streamType);
|
|
526
|
+
});
|
|
527
|
+
ws.on("error", (error) => {
|
|
528
|
+
log2(`${streamType} stream error: ${error.message}`, { type: "error" });
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
getCloseCodeDescription(code) {
|
|
532
|
+
const codes = {
|
|
533
|
+
1e3: "Normal Closure",
|
|
534
|
+
1001: "Going Away",
|
|
535
|
+
1002: "Protocol Error",
|
|
536
|
+
1003: "Unsupported Data",
|
|
537
|
+
1005: "No Status Received",
|
|
538
|
+
1006: "Abnormal Closure",
|
|
539
|
+
1007: "Invalid Frame Payload Data",
|
|
540
|
+
1008: "Policy Violation",
|
|
541
|
+
1009: "Message Too Big",
|
|
542
|
+
1010: "Mandatory Extension",
|
|
543
|
+
1011: "Internal Server Error",
|
|
544
|
+
1012: "Service Restart",
|
|
545
|
+
1013: "Try Again Later",
|
|
546
|
+
1014: "Bad Gateway",
|
|
547
|
+
1015: "TLS Handshake"
|
|
548
|
+
};
|
|
549
|
+
return codes[code] || "Unknown";
|
|
550
|
+
}
|
|
551
|
+
sendSubscription(streamType) {
|
|
552
|
+
const ws = streamType === "stock" ? this.stockWs : this.optionWs;
|
|
553
|
+
const subscriptions = streamType === "stock" ? this.stockSubscriptions : this.optionSubscriptions;
|
|
554
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
555
|
+
const subMessagePayload = {};
|
|
556
|
+
if (subscriptions.trades.length > 0) {
|
|
557
|
+
subMessagePayload.trades = subscriptions.trades;
|
|
558
|
+
}
|
|
559
|
+
if (subscriptions.quotes.length > 0) {
|
|
560
|
+
subMessagePayload.quotes = subscriptions.quotes;
|
|
561
|
+
}
|
|
562
|
+
if (subscriptions.bars.length > 0) {
|
|
563
|
+
subMessagePayload.bars = subscriptions.bars;
|
|
564
|
+
}
|
|
565
|
+
if (Object.keys(subMessagePayload).length > 0) {
|
|
566
|
+
const subMessage = {
|
|
567
|
+
action: "subscribe",
|
|
568
|
+
...subMessagePayload
|
|
569
|
+
};
|
|
570
|
+
ws.send(JSON.stringify(subMessage));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
connectStockStream() {
|
|
575
|
+
if (!this.stockWs) {
|
|
576
|
+
this.connect("stock");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
connectOptionStream() {
|
|
580
|
+
if (!this.optionWs) {
|
|
581
|
+
this.connect("option");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
disconnectStockStream() {
|
|
585
|
+
if (this.stockReconnectTimeout) {
|
|
586
|
+
clearTimeout(this.stockReconnectTimeout);
|
|
587
|
+
this.stockReconnectTimeout = null;
|
|
588
|
+
}
|
|
589
|
+
this.stockReconnectAttempts = 0;
|
|
590
|
+
if (this.stockWs) {
|
|
591
|
+
this.stockWs.close();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
disconnectOptionStream() {
|
|
595
|
+
if (this.optionReconnectTimeout) {
|
|
596
|
+
clearTimeout(this.optionReconnectTimeout);
|
|
597
|
+
this.optionReconnectTimeout = null;
|
|
598
|
+
}
|
|
599
|
+
this.optionReconnectAttempts = 0;
|
|
600
|
+
if (this.optionWs) {
|
|
601
|
+
this.optionWs.close();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
subscribe(streamType, subscriptions) {
|
|
605
|
+
const currentSubscriptions = streamType === "stock" ? this.stockSubscriptions : this.optionSubscriptions;
|
|
606
|
+
Object.entries(subscriptions).forEach(([key, value]) => {
|
|
607
|
+
if (value) {
|
|
608
|
+
currentSubscriptions[key] = [.../* @__PURE__ */ new Set([...currentSubscriptions[key] || [], ...value])];
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
this.sendSubscription(streamType);
|
|
612
|
+
}
|
|
613
|
+
unsubscribe(streamType, subscriptions) {
|
|
614
|
+
const currentSubscriptions = streamType === "stock" ? this.stockSubscriptions : this.optionSubscriptions;
|
|
615
|
+
Object.entries(subscriptions).forEach(([key, value]) => {
|
|
616
|
+
if (value) {
|
|
617
|
+
currentSubscriptions[key] = (currentSubscriptions[key] || []).filter((s) => !value.includes(s));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
const unsubMessage = {
|
|
621
|
+
action: "unsubscribe",
|
|
622
|
+
...subscriptions
|
|
623
|
+
};
|
|
624
|
+
const ws = streamType === "stock" ? this.stockWs : this.optionWs;
|
|
625
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
626
|
+
ws.send(JSON.stringify(unsubMessage));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async makeRequest(endpoint, method = "GET", params, baseUrlName = "data") {
|
|
630
|
+
const baseUrl = baseUrlName === "data" ? this.dataURL : baseUrlName === "api" ? this.apiURL : baseUrlName === "v1beta1" ? this.v1beta1url : this.v1beta3url;
|
|
631
|
+
const url = new URL(`${baseUrl}${endpoint}`);
|
|
632
|
+
try {
|
|
633
|
+
if (params) {
|
|
634
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
635
|
+
if (Array.isArray(value)) {
|
|
636
|
+
url.searchParams.append(key, value.join(","));
|
|
637
|
+
} else if (value !== void 0) {
|
|
638
|
+
url.searchParams.append(key, value.toString());
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const response = await fetch(url.toString(), {
|
|
643
|
+
method,
|
|
644
|
+
headers: this.headers
|
|
645
|
+
});
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
const errorText = await response.text();
|
|
648
|
+
log2(`Market Data API error (${response.status}): ${errorText}`, { type: "error" });
|
|
649
|
+
throw new Error(`Market Data API error (${response.status}): ${errorText}`);
|
|
650
|
+
}
|
|
651
|
+
const data = await response.json();
|
|
652
|
+
return data;
|
|
653
|
+
} catch (err) {
|
|
654
|
+
const error = err;
|
|
655
|
+
log2(`Error in makeRequest: ${error.message}. Endpoint: ${endpoint}. Url: ${url.toString()}`, { type: "error" });
|
|
656
|
+
if (error instanceof TypeError) {
|
|
657
|
+
log2(`Network error details: ${error.stack}`, { type: "error" });
|
|
658
|
+
}
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get historical OHLCV bars for specified symbols, including pre-market and post-market data
|
|
664
|
+
* Automatically handles pagination to fetch all available data
|
|
665
|
+
* @param params Parameters for historical bars request
|
|
666
|
+
* @returns Historical bars data with all pages combined
|
|
667
|
+
*/
|
|
668
|
+
async getHistoricalBars(params) {
|
|
669
|
+
const symbols = params.symbols;
|
|
670
|
+
const symbolsStr = symbols.join(",");
|
|
671
|
+
let allBars = {};
|
|
672
|
+
let pageToken = null;
|
|
673
|
+
let hasMorePages = true;
|
|
674
|
+
let totalBarsCount = 0;
|
|
675
|
+
let pageCount = 0;
|
|
676
|
+
let currency = "";
|
|
677
|
+
symbols.forEach((symbol) => {
|
|
678
|
+
allBars[symbol] = [];
|
|
679
|
+
});
|
|
680
|
+
log2(
|
|
681
|
+
`Starting ${params.timeframe}bars fetch for ${symbols.length} symbols (${symbolsStr}), ${params.start || "no start"} to ${params.end || "no end"})`,
|
|
682
|
+
{ symbol: symbolsStr }
|
|
683
|
+
);
|
|
684
|
+
while (hasMorePages) {
|
|
685
|
+
pageCount++;
|
|
686
|
+
const requestParams = {
|
|
687
|
+
...params,
|
|
688
|
+
adjustment: params.adjustment ?? DEFAULT_ADJUSTMENT,
|
|
689
|
+
feed: DEFAULT_FEED,
|
|
690
|
+
...pageToken && { page_token: pageToken }
|
|
691
|
+
};
|
|
692
|
+
const response = await this.makeRequest("/stocks/bars", "GET", requestParams);
|
|
693
|
+
if (!response.bars) {
|
|
694
|
+
log2(`No bars data found in response for ${symbols.length} symbols`, { symbol: symbolsStr, type: "warn" });
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
if (!currency) {
|
|
698
|
+
currency = response.currency;
|
|
699
|
+
}
|
|
700
|
+
let pageBarsCount = 0;
|
|
701
|
+
let earliestTimestamp = null;
|
|
702
|
+
let latestTimestamp = null;
|
|
703
|
+
Object.entries(response.bars).forEach(([symbol, bars]) => {
|
|
704
|
+
if (bars && bars.length > 0) {
|
|
705
|
+
allBars[symbol] = [...allBars[symbol], ...bars];
|
|
706
|
+
pageBarsCount += bars.length;
|
|
707
|
+
bars.forEach((bar) => {
|
|
708
|
+
const barDate = new Date(bar.t);
|
|
709
|
+
if (!earliestTimestamp || barDate < earliestTimestamp) {
|
|
710
|
+
earliestTimestamp = barDate;
|
|
711
|
+
}
|
|
712
|
+
if (!latestTimestamp || barDate > latestTimestamp) {
|
|
713
|
+
latestTimestamp = barDate;
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
totalBarsCount += pageBarsCount;
|
|
719
|
+
pageToken = response.next_page_token || null;
|
|
720
|
+
hasMorePages = !!pageToken;
|
|
721
|
+
const dateRangeStr = earliestTimestamp && latestTimestamp ? `${new Date(earliestTimestamp).toLocaleDateString("en-US", { timeZone: "America/New_York" })} to ${new Date(
|
|
722
|
+
latestTimestamp
|
|
723
|
+
).toLocaleDateString("en-US", { timeZone: "America/New_York" })}` : "unknown range";
|
|
724
|
+
log2(
|
|
725
|
+
`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} bars (total: ${totalBarsCount.toLocaleString()}) for ${symbols.length} symbols, date range: ${dateRangeStr}${hasMorePages ? ", more pages available" : ", complete"}`,
|
|
726
|
+
{ symbol: symbolsStr, type: "debug" }
|
|
727
|
+
);
|
|
728
|
+
if (pageCount > 1e3) {
|
|
729
|
+
log2(`Stopping pagination after ${pageCount} pages to prevent infinite loop`, { type: "warn" });
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(", ");
|
|
734
|
+
log2(
|
|
735
|
+
`${params.timeframe} bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages for ${symbols.length} symbols (${symbolCounts})`,
|
|
736
|
+
{ symbol: symbolsStr }
|
|
737
|
+
);
|
|
738
|
+
return {
|
|
739
|
+
bars: allBars,
|
|
740
|
+
next_page_token: null,
|
|
741
|
+
// Always null since we fetch all pages
|
|
742
|
+
currency: currency || DEFAULT_CURRENCY
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get the most recent minute bar for requested symbols
|
|
747
|
+
* @param symbols Array of stock symbols to query
|
|
748
|
+
* @param currency Optional currency in ISO 4217 format
|
|
749
|
+
* @returns Latest bar data for each symbol
|
|
750
|
+
|
|
751
|
+
*/
|
|
752
|
+
async getLatestBars(symbols, currency) {
|
|
753
|
+
return this.makeRequest("/stocks/bars/latest", "GET", {
|
|
754
|
+
symbols,
|
|
755
|
+
feed: DEFAULT_FEED,
|
|
756
|
+
currency: currency || DEFAULT_CURRENCY
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Get the last trade for a single symbol
|
|
761
|
+
* @param symbol The stock symbol to query
|
|
762
|
+
* @returns Last trade details including price, size, exchange, and conditions
|
|
763
|
+
*/
|
|
764
|
+
async getLastTrade(symbol) {
|
|
765
|
+
return this.makeRequest(`/v1/last/stocks/${symbol}`, "GET");
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get the most recent trades for requested symbols
|
|
769
|
+
* @param symbols Array of stock symbols to query
|
|
770
|
+
* @param feed Optional data source (sip/iex/delayed_sip)
|
|
771
|
+
* @param currency Optional currency in ISO 4217 format
|
|
772
|
+
* @returns Latest trade data for each symbol
|
|
773
|
+
|
|
774
|
+
*/
|
|
775
|
+
async getLatestTrades(symbols, feed, currency) {
|
|
776
|
+
return this.makeRequest("/stocks/trades/latest", "GET", {
|
|
777
|
+
symbols,
|
|
778
|
+
feed: feed || DEFAULT_FEED,
|
|
779
|
+
currency: currency || DEFAULT_CURRENCY
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get the most recent quotes for requested symbols
|
|
784
|
+
* @param symbols Array of stock symbols to query
|
|
785
|
+
* @param feed Optional data source (sip/iex/delayed_sip)
|
|
786
|
+
* @param currency Optional currency in ISO 4217 format
|
|
787
|
+
* @returns Latest quote data for each symbol
|
|
788
|
+
*/
|
|
789
|
+
async getLatestQuotes(symbols, feed, currency) {
|
|
790
|
+
if (!symbols || symbols.length === 0) {
|
|
791
|
+
log2("No symbols provided to getLatestQuotes, returning empty response", { type: "warn" });
|
|
792
|
+
return {
|
|
793
|
+
quotes: {},
|
|
794
|
+
currency: currency || DEFAULT_CURRENCY
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return this.makeRequest("/stocks/quotes/latest", "GET", {
|
|
798
|
+
symbols,
|
|
799
|
+
feed: feed || DEFAULT_FEED,
|
|
800
|
+
currency: currency || DEFAULT_CURRENCY
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Get the latest quote for a single symbol
|
|
805
|
+
* @param symbol The stock symbol to query
|
|
806
|
+
* @param feed Optional data source (sip/iex/delayed_sip)
|
|
807
|
+
* @param currency Optional currency in ISO 4217 format
|
|
808
|
+
* @returns Latest quote data with symbol and currency information
|
|
809
|
+
*/
|
|
810
|
+
async getLatestQuote(symbol, feed, currency) {
|
|
811
|
+
return this.makeRequest(`/stocks/${symbol}/quotes/latest`, "GET", {
|
|
812
|
+
feed: feed || DEFAULT_FEED,
|
|
813
|
+
currency
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get the previous day's closing price for a symbol
|
|
818
|
+
* @param symbol The stock symbol to query
|
|
819
|
+
* @param referenceDate Optional reference date to get the previous close for
|
|
820
|
+
* @returns Previous day's closing price data
|
|
821
|
+
*/
|
|
822
|
+
async getPreviousClose(symbol, referenceDate) {
|
|
823
|
+
const date = referenceDate || /* @__PURE__ */ new Date();
|
|
824
|
+
const prevMarketDate = getLastFullTradingDate(date);
|
|
825
|
+
const response = await this.getHistoricalBars({
|
|
826
|
+
symbols: [symbol],
|
|
827
|
+
timeframe: "1Day",
|
|
828
|
+
start: prevMarketDate.toISOString(),
|
|
829
|
+
end: prevMarketDate.toISOString(),
|
|
830
|
+
limit: 1
|
|
831
|
+
});
|
|
832
|
+
if (!response.bars[symbol] || response.bars[symbol].length === 0) {
|
|
833
|
+
log2(`No previous close data available for ${symbol}`, { type: "error", symbol });
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
return response.bars[symbol][0];
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Get hourly price data for a symbol
|
|
840
|
+
* @param symbol The stock symbol to query
|
|
841
|
+
* @param start Start time in milliseconds
|
|
842
|
+
* @param end End time in milliseconds
|
|
843
|
+
* @returns Array of hourly price bars
|
|
844
|
+
*/
|
|
845
|
+
async getHourlyPrices(symbol, start, end) {
|
|
846
|
+
const response = await this.getHistoricalBars({
|
|
847
|
+
symbols: [symbol],
|
|
848
|
+
timeframe: "1Hour",
|
|
849
|
+
start: new Date(start).toISOString(),
|
|
850
|
+
end: new Date(end).toISOString(),
|
|
851
|
+
limit: 96
|
|
852
|
+
// Last 96 hours (4 days)
|
|
853
|
+
});
|
|
854
|
+
return response.bars[symbol] || [];
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Get half-hourly price data for a symbol
|
|
858
|
+
* @param symbol The stock symbol to query
|
|
859
|
+
* @param start Start time in milliseconds
|
|
860
|
+
* @param end End time in milliseconds
|
|
861
|
+
* @returns Array of half-hourly price bars
|
|
862
|
+
*/
|
|
863
|
+
async getHalfHourlyPrices(symbol, start, end) {
|
|
864
|
+
const response = await this.getHistoricalBars({
|
|
865
|
+
symbols: [symbol],
|
|
866
|
+
timeframe: "30Min",
|
|
867
|
+
start: new Date(start).toISOString(),
|
|
868
|
+
end: new Date(end).toISOString(),
|
|
869
|
+
limit: 16 * 2 * 4
|
|
870
|
+
// last 4 days, 16 hours per day, 2 bars per hour
|
|
871
|
+
});
|
|
872
|
+
return response.bars[symbol] || [];
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Get daily price data for a symbol
|
|
876
|
+
* @param symbol The stock symbol to query
|
|
877
|
+
* @param start Start time in milliseconds
|
|
878
|
+
* @param end End time in milliseconds
|
|
879
|
+
* @returns Array of daily price bars
|
|
880
|
+
*/
|
|
881
|
+
async getDailyPrices(symbol, start, end) {
|
|
882
|
+
const response = await this.getHistoricalBars({
|
|
883
|
+
symbols: [symbol],
|
|
884
|
+
timeframe: "1Day",
|
|
885
|
+
start: new Date(start).toISOString(),
|
|
886
|
+
end: new Date(end).toISOString(),
|
|
887
|
+
limit: 100
|
|
888
|
+
// Last 100 days
|
|
889
|
+
});
|
|
890
|
+
return response.bars[symbol] || [];
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Get intraday price data for a symbol
|
|
894
|
+
* @param symbol The stock symbol to query
|
|
895
|
+
* @param minutePeriod Minutes per bar (1, 5, 15, etc.)
|
|
896
|
+
* @param start Start time in milliseconds
|
|
897
|
+
* @param end End time in milliseconds
|
|
898
|
+
* @returns Array of intraday price bars
|
|
899
|
+
*/
|
|
900
|
+
async getIntradayPrices(symbol, minutePeriod, start, end) {
|
|
901
|
+
const timeframe = `${minutePeriod}Min`;
|
|
902
|
+
const response = await this.getHistoricalBars({
|
|
903
|
+
symbols: [symbol],
|
|
904
|
+
timeframe,
|
|
905
|
+
start: new Date(start).toISOString(),
|
|
906
|
+
end: new Date(end).toISOString()
|
|
907
|
+
});
|
|
908
|
+
return response.bars[symbol] || [];
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Analyzes an array of price bars and returns a summary string
|
|
912
|
+
* @param bars Array of price bars to analyze
|
|
913
|
+
* @returns A string summarizing the price data
|
|
914
|
+
*/
|
|
915
|
+
static analyzeBars(bars) {
|
|
916
|
+
if (!bars || bars.length === 0) {
|
|
917
|
+
return "No price data available";
|
|
918
|
+
}
|
|
919
|
+
const firstBar = bars[0];
|
|
920
|
+
const lastBar = bars[bars.length - 1];
|
|
921
|
+
const priceChange = lastBar.c - firstBar.o;
|
|
922
|
+
const percentChange = priceChange / firstBar.o * 100;
|
|
923
|
+
const volumeChange = lastBar.v - firstBar.v;
|
|
924
|
+
const percentVolumeChange = volumeChange / firstBar.v * 100;
|
|
925
|
+
const high = Math.max(...bars.map((bar) => bar.h));
|
|
926
|
+
const low = Math.min(...bars.map((bar) => bar.l));
|
|
927
|
+
const totalVolume = bars.reduce((sum, bar) => sum + bar.v, 0);
|
|
928
|
+
const avgVolume = totalVolume / bars.length;
|
|
929
|
+
return `Price: $${firstBar.o.toFixed(2)} -> $${lastBar.c.toFixed(2)} (${percentChange.toFixed(2)}%), Volume: ${firstBar.v.toLocaleString()} -> ${lastBar.v.toLocaleString()} (${percentVolumeChange.toFixed(2)}%), High: $${high.toFixed(2)}, Low: $${low.toFixed(2)}, Avg Volume: ${avgVolume.toLocaleString()}`;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Get assets available for trade and data consumption from Alpaca
|
|
933
|
+
* @param params Optional query params
|
|
934
|
+
* - status: 'active' | 'inactive' (default 'active')
|
|
935
|
+
* - asset_class: 'us_equity' | 'us_option' | 'crypto' (default 'us_equity')
|
|
936
|
+
* - shortable: if true (default), filters to assets with shortable=true and easy_to_borrow=true
|
|
937
|
+
* @returns Array of AlpacaAsset objects
|
|
938
|
+
* @see https://docs.alpaca.markets/reference/get-v2-assets-1
|
|
939
|
+
*/
|
|
940
|
+
async getAssets(params) {
|
|
941
|
+
const {
|
|
942
|
+
status = "active",
|
|
943
|
+
asset_class = "us_equity",
|
|
944
|
+
shortable = true
|
|
945
|
+
} = {
|
|
946
|
+
status: params?.status ?? "active",
|
|
947
|
+
asset_class: params?.asset_class ?? "us_equity",
|
|
948
|
+
shortable: params?.shortable ?? true
|
|
949
|
+
};
|
|
950
|
+
const assets = await this.makeRequest(
|
|
951
|
+
"/assets",
|
|
952
|
+
"GET",
|
|
953
|
+
{ status, asset_class },
|
|
954
|
+
"api"
|
|
955
|
+
);
|
|
956
|
+
if (!shortable) return assets;
|
|
957
|
+
return assets.filter((a) => a.shortable === true && a.easy_to_borrow === true);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Get a single asset by symbol or asset_id
|
|
961
|
+
* @param symbolOrAssetId Symbol or asset_id
|
|
962
|
+
* @returns AlpacaAsset object
|
|
963
|
+
* @see https://docs.alpaca.markets/reference/get-v2-assets-symbol_or_asset_id
|
|
964
|
+
*/
|
|
965
|
+
async getAsset(symbolOrAssetId) {
|
|
966
|
+
return this.makeRequest(`/assets/${encodeURIComponent(symbolOrAssetId)}`, "GET", void 0, "api");
|
|
967
|
+
}
|
|
968
|
+
// ===== OPTIONS MARKET DATA METHODS =====
|
|
969
|
+
/**
|
|
970
|
+
* Get options chain for an underlying symbol
|
|
971
|
+
* Provides the latest trade, latest quote, and greeks for each contract symbol of the underlying symbol
|
|
972
|
+
* @param params Options chain request parameters
|
|
973
|
+
* @returns Options chain data with snapshots for each contract
|
|
974
|
+
* @see https://docs.alpaca.markets/reference/optionchain
|
|
975
|
+
*/
|
|
976
|
+
async getOptionsChain(params) {
|
|
977
|
+
const { underlying_symbol, ...queryParams } = params;
|
|
978
|
+
return this.makeRequest(
|
|
979
|
+
`/options/snapshots/${encodeURIComponent(underlying_symbol)}`,
|
|
980
|
+
"GET",
|
|
981
|
+
queryParams,
|
|
982
|
+
"v1beta1"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Get the most recent trades for requested option contract symbols
|
|
987
|
+
* @param params Latest options trades request parameters
|
|
988
|
+
* @returns Latest trade data for each option contract symbol
|
|
989
|
+
|
|
990
|
+
* @see https://docs.alpaca.markets/reference/optionlatesttrades
|
|
991
|
+
*/
|
|
992
|
+
async getLatestOptionsTrades(params) {
|
|
993
|
+
const { limit, page_token, ...requestParams } = params;
|
|
994
|
+
return this.makeRequest("/options/trades/latest", "GET", requestParams, "v1beta1");
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Get the most recent quotes for requested option contract symbols
|
|
998
|
+
* @param params Latest options quotes request parameters
|
|
999
|
+
* @returns Latest quote data for each option contract symbol
|
|
1000
|
+
|
|
1001
|
+
* @see https://docs.alpaca.markets/reference/optionlatestquotes
|
|
1002
|
+
*/
|
|
1003
|
+
async getLatestOptionsQuotes(params) {
|
|
1004
|
+
const { limit, page_token, ...requestParams } = params;
|
|
1005
|
+
return this.makeRequest("/options/quotes/latest", "GET", requestParams, "v1beta1");
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Get historical OHLCV bars for option contract symbols
|
|
1009
|
+
* Automatically handles pagination to fetch all available data
|
|
1010
|
+
* @param params Historical options bars request parameters
|
|
1011
|
+
* @returns Historical bar data for each option contract symbol with all pages combined
|
|
1012
|
+
|
|
1013
|
+
* @see https://docs.alpaca.markets/reference/optionbars
|
|
1014
|
+
*/
|
|
1015
|
+
async getHistoricalOptionsBars(params) {
|
|
1016
|
+
const symbols = params.symbols;
|
|
1017
|
+
const symbolsStr = symbols.join(",");
|
|
1018
|
+
let allBars = {};
|
|
1019
|
+
let pageToken = null;
|
|
1020
|
+
let hasMorePages = true;
|
|
1021
|
+
let totalBarsCount = 0;
|
|
1022
|
+
let pageCount = 0;
|
|
1023
|
+
symbols.forEach((symbol) => {
|
|
1024
|
+
allBars[symbol] = [];
|
|
1025
|
+
});
|
|
1026
|
+
log2(
|
|
1027
|
+
`Starting historical options bars fetch for ${symbols.length} symbols (${params.timeframe}, ${params.start || "no start"} to ${params.end || "no end"})`
|
|
1028
|
+
);
|
|
1029
|
+
while (hasMorePages) {
|
|
1030
|
+
pageCount++;
|
|
1031
|
+
const requestParams = {
|
|
1032
|
+
...params,
|
|
1033
|
+
...pageToken && { page_token: pageToken }
|
|
1034
|
+
};
|
|
1035
|
+
const response = await this.makeRequest(
|
|
1036
|
+
"/options/bars",
|
|
1037
|
+
"GET",
|
|
1038
|
+
requestParams,
|
|
1039
|
+
"v1beta1"
|
|
1040
|
+
);
|
|
1041
|
+
if (!response.bars) {
|
|
1042
|
+
log2(`No options bars data found in response for ${symbols.length} symbols`, { type: "warn" });
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
let pageBarsCount = 0;
|
|
1046
|
+
let earliestTimestamp = null;
|
|
1047
|
+
let latestTimestamp = null;
|
|
1048
|
+
Object.entries(response.bars).forEach(([symbol, bars]) => {
|
|
1049
|
+
if (bars && bars.length > 0) {
|
|
1050
|
+
allBars[symbol] = [...allBars[symbol], ...bars];
|
|
1051
|
+
pageBarsCount += bars.length;
|
|
1052
|
+
bars.forEach((bar) => {
|
|
1053
|
+
const barDate = new Date(bar.t);
|
|
1054
|
+
if (!earliestTimestamp || barDate < earliestTimestamp) {
|
|
1055
|
+
earliestTimestamp = barDate;
|
|
1056
|
+
}
|
|
1057
|
+
if (!latestTimestamp || barDate > latestTimestamp) {
|
|
1058
|
+
latestTimestamp = barDate;
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
totalBarsCount += pageBarsCount;
|
|
1064
|
+
pageToken = response.next_page_token || null;
|
|
1065
|
+
hasMorePages = !!pageToken;
|
|
1066
|
+
const dateRangeStr = earliestTimestamp && latestTimestamp ? `${earliestTimestamp.toLocaleDateString("en-US", { timeZone: "America/New_York" })} to ${latestTimestamp.toLocaleDateString("en-US", { timeZone: "America/New_York" })}` : "unknown range";
|
|
1067
|
+
log2(
|
|
1068
|
+
`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} option bars (total: ${totalBarsCount.toLocaleString()}) for ${symbols.length} symbols, date range: ${dateRangeStr}${hasMorePages ? ", more pages available" : ", complete"}`,
|
|
1069
|
+
{
|
|
1070
|
+
type: "debug"
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
if (pageCount > 1e3) {
|
|
1074
|
+
log2(`Stopping options bars pagination after ${pageCount} pages to prevent infinite loop`, { type: "warn" });
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(", ");
|
|
1079
|
+
log2(
|
|
1080
|
+
`Options bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`
|
|
1081
|
+
);
|
|
1082
|
+
return {
|
|
1083
|
+
bars: allBars,
|
|
1084
|
+
next_page_token: void 0
|
|
1085
|
+
// Always undefined since we fetch all pages
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Get historical trades for option contract symbols
|
|
1090
|
+
* Automatically handles pagination to fetch all available data
|
|
1091
|
+
* @param params Historical options trades request parameters
|
|
1092
|
+
* @returns Historical trade data for each option contract symbol with all pages combined
|
|
1093
|
+
|
|
1094
|
+
* @see https://docs.alpaca.markets/reference/optiontrades
|
|
1095
|
+
*/
|
|
1096
|
+
async getHistoricalOptionsTrades(params) {
|
|
1097
|
+
const symbols = params.symbols;
|
|
1098
|
+
const symbolsStr = symbols.join(",");
|
|
1099
|
+
let allTrades = {};
|
|
1100
|
+
let pageToken = null;
|
|
1101
|
+
let hasMorePages = true;
|
|
1102
|
+
let totalTradesCount = 0;
|
|
1103
|
+
let pageCount = 0;
|
|
1104
|
+
symbols.forEach((symbol) => {
|
|
1105
|
+
allTrades[symbol] = [];
|
|
1106
|
+
});
|
|
1107
|
+
log2(
|
|
1108
|
+
`Starting historical options trades fetch for ${symbols.length} symbols (${params.start || "no start"} to ${params.end || "no end"})`
|
|
1109
|
+
);
|
|
1110
|
+
while (hasMorePages) {
|
|
1111
|
+
pageCount++;
|
|
1112
|
+
const requestParams = {
|
|
1113
|
+
...params,
|
|
1114
|
+
...pageToken && { page_token: pageToken }
|
|
1115
|
+
};
|
|
1116
|
+
const response = await this.makeRequest(
|
|
1117
|
+
"/options/trades",
|
|
1118
|
+
"GET",
|
|
1119
|
+
requestParams,
|
|
1120
|
+
"v1beta1"
|
|
1121
|
+
);
|
|
1122
|
+
if (!response.trades) {
|
|
1123
|
+
log2(`No options trades data found in response for ${symbols.length} symbols`, { type: "warn" });
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
let pageTradesCount = 0;
|
|
1127
|
+
let earliestTimestamp = null;
|
|
1128
|
+
let latestTimestamp = null;
|
|
1129
|
+
Object.entries(response.trades).forEach(([symbol, trades]) => {
|
|
1130
|
+
if (trades && trades.length > 0) {
|
|
1131
|
+
allTrades[symbol] = [...allTrades[symbol], ...trades];
|
|
1132
|
+
pageTradesCount += trades.length;
|
|
1133
|
+
trades.forEach((trade) => {
|
|
1134
|
+
const tradeDate = new Date(trade.t);
|
|
1135
|
+
if (!earliestTimestamp || tradeDate < earliestTimestamp) {
|
|
1136
|
+
earliestTimestamp = tradeDate;
|
|
1137
|
+
}
|
|
1138
|
+
if (!latestTimestamp || tradeDate > latestTimestamp) {
|
|
1139
|
+
latestTimestamp = tradeDate;
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
totalTradesCount += pageTradesCount;
|
|
1145
|
+
pageToken = response.next_page_token || null;
|
|
1146
|
+
hasMorePages = !!pageToken;
|
|
1147
|
+
const dateRangeStr = earliestTimestamp && latestTimestamp ? `${earliestTimestamp.toLocaleDateString("en-US", { timeZone: "America/New_York" })} to ${latestTimestamp.toLocaleDateString("en-US", { timeZone: "America/New_York" })}` : "unknown range";
|
|
1148
|
+
log2(
|
|
1149
|
+
`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} option trades (total: ${totalTradesCount.toLocaleString()}) for ${symbols.length} symbols, date range: ${dateRangeStr}${hasMorePages ? ", more pages available" : ", complete"}`,
|
|
1150
|
+
{
|
|
1151
|
+
type: "debug"
|
|
1152
|
+
}
|
|
1153
|
+
);
|
|
1154
|
+
if (pageCount > 1e3) {
|
|
1155
|
+
log2(`Stopping options trades pagination after ${pageCount} pages to prevent infinite loop`, { type: "warn" });
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const symbolCounts = Object.entries(allTrades).map(([symbol, trades]) => `${symbol}: ${trades.length}`).join(", ");
|
|
1160
|
+
log2(
|
|
1161
|
+
`Options trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages (${symbolCounts})`
|
|
1162
|
+
);
|
|
1163
|
+
return {
|
|
1164
|
+
trades: allTrades,
|
|
1165
|
+
next_page_token: void 0
|
|
1166
|
+
// Always undefined since we fetch all pages
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Get snapshots for option contract symbols
|
|
1171
|
+
* Provides latest trade, latest quote, and greeks for each contract symbol
|
|
1172
|
+
* @param params Options snapshots request parameters
|
|
1173
|
+
* @returns Snapshot data for each option contract symbol
|
|
1174
|
+
|
|
1175
|
+
* @see https://docs.alpaca.markets/reference/optionsnapshots
|
|
1176
|
+
*/
|
|
1177
|
+
async getOptionsSnapshot(params) {
|
|
1178
|
+
const { limit, page_token, ...requestParams } = params;
|
|
1179
|
+
return this.makeRequest("/options/snapshots", "GET", requestParams, "v1beta1");
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Get condition codes for options trades or quotes
|
|
1183
|
+
* Returns the mapping between condition codes and their descriptions
|
|
1184
|
+
* @param tickType The type of tick data ('trade' or 'quote')
|
|
1185
|
+
* @returns Mapping of condition codes to descriptions
|
|
1186
|
+
|
|
1187
|
+
* @see https://docs.alpaca.markets/reference/optionmetaconditions
|
|
1188
|
+
*/
|
|
1189
|
+
async getOptionsConditionCodes(tickType) {
|
|
1190
|
+
return this.makeRequest(`/options/meta/conditions/${tickType}`, "GET", void 0, "v1beta1");
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Get exchange codes for options
|
|
1194
|
+
* Returns the mapping between option exchange codes and exchange names
|
|
1195
|
+
* @returns Mapping of exchange codes to exchange names
|
|
1196
|
+
|
|
1197
|
+
* @see https://docs.alpaca.markets/reference/optionmetaexchanges
|
|
1198
|
+
*/
|
|
1199
|
+
async getOptionsExchangeCodes() {
|
|
1200
|
+
return this.makeRequest("/options/meta/exchanges", "GET", void 0, "v1beta1");
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Analyzes an array of option bars and returns a summary string
|
|
1204
|
+
* @param bars Array of option bars to analyze
|
|
1205
|
+
* @returns A string summarizing the option price data
|
|
1206
|
+
*/
|
|
1207
|
+
static analyzeOptionBars(bars) {
|
|
1208
|
+
if (!bars || bars.length === 0) {
|
|
1209
|
+
return "No option price data available";
|
|
1210
|
+
}
|
|
1211
|
+
const firstBar = bars[0];
|
|
1212
|
+
const lastBar = bars[bars.length - 1];
|
|
1213
|
+
const priceChange = lastBar.c - firstBar.o;
|
|
1214
|
+
const percentChange = priceChange / firstBar.o * 100;
|
|
1215
|
+
const volumeChange = lastBar.v - firstBar.v;
|
|
1216
|
+
const percentVolumeChange = firstBar.v > 0 ? volumeChange / firstBar.v * 100 : 0;
|
|
1217
|
+
const high = Math.max(...bars.map((bar) => bar.h));
|
|
1218
|
+
const low = Math.min(...bars.map((bar) => bar.l));
|
|
1219
|
+
const totalVolume = bars.reduce((sum, bar) => sum + bar.v, 0);
|
|
1220
|
+
const avgVolume = totalVolume / bars.length;
|
|
1221
|
+
return `Option Price: $${firstBar.o.toFixed(2)} -> $${lastBar.c.toFixed(2)} (${percentChange.toFixed(2)}%), Volume: ${firstBar.v.toLocaleString()} -> ${lastBar.v.toLocaleString()} (${percentVolumeChange.toFixed(2)}%), High: $${high.toFixed(2)}, Low: $${low.toFixed(2)}, Avg Volume: ${avgVolume.toLocaleString()}`;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Formats option greeks for display
|
|
1225
|
+
* @param greeks Option greeks object
|
|
1226
|
+
* @returns Formatted string with greek values
|
|
1227
|
+
*/
|
|
1228
|
+
static formatOptionGreeks(greeks) {
|
|
1229
|
+
if (!greeks) {
|
|
1230
|
+
return "No greeks data available";
|
|
1231
|
+
}
|
|
1232
|
+
const parts = [];
|
|
1233
|
+
if (greeks.delta !== void 0) parts.push(`Delta: ${greeks.delta.toFixed(4)}`);
|
|
1234
|
+
if (greeks.gamma !== void 0) parts.push(`Gamma: ${greeks.gamma.toFixed(4)}`);
|
|
1235
|
+
if (greeks.theta !== void 0) parts.push(`Theta: ${greeks.theta.toFixed(4)}`);
|
|
1236
|
+
if (greeks.vega !== void 0) parts.push(`Vega: ${greeks.vega.toFixed(4)}`);
|
|
1237
|
+
if (greeks.rho !== void 0) parts.push(`Rho: ${greeks.rho.toFixed(4)}`);
|
|
1238
|
+
return parts.length > 0 ? parts.join(", ") : "No greeks data available";
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Interprets condition codes using the provided condition codes mapping
|
|
1242
|
+
* @param conditionCodes Array of condition codes from trade or quote
|
|
1243
|
+
* @param conditionCodesMap Mapping of condition codes to descriptions
|
|
1244
|
+
* @returns Formatted string with condition descriptions
|
|
1245
|
+
*/
|
|
1246
|
+
static interpretConditionCodes(conditionCodes, conditionCodesMap) {
|
|
1247
|
+
if (!conditionCodes || conditionCodes.length === 0) {
|
|
1248
|
+
return "No conditions";
|
|
1249
|
+
}
|
|
1250
|
+
const descriptions = conditionCodes.map((code) => conditionCodesMap[code] || `Unknown (${code})`).filter((desc) => desc !== void 0);
|
|
1251
|
+
return descriptions.length > 0 ? descriptions.join(", ") : "No condition descriptions available";
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Gets the exchange name from exchange code using the provided exchange codes mapping
|
|
1255
|
+
* @param exchangeCode Exchange code from trade or quote
|
|
1256
|
+
* @param exchangeCodesMap Mapping of exchange codes to names
|
|
1257
|
+
* @returns Exchange name or formatted unknown exchange
|
|
1258
|
+
*/
|
|
1259
|
+
static getExchangeName(exchangeCode, exchangeCodesMap) {
|
|
1260
|
+
return exchangeCodesMap[exchangeCode] || `Unknown Exchange (${exchangeCode})`;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Fetches news articles from Alpaca API for a symbol, paginating through all results.
|
|
1264
|
+
* @param symbol The symbol to fetch news for (e.g., 'AAPL')
|
|
1265
|
+
* @param params Optional parameters: start, end, limit, sort, include_content
|
|
1266
|
+
* @returns Array of SimpleNews articles
|
|
1267
|
+
*/
|
|
1268
|
+
async fetchNews(symbol, params) {
|
|
1269
|
+
const defaultParams = {
|
|
1270
|
+
start: new Date(Date.now() - 24 * 60 * 60 * 1e3),
|
|
1271
|
+
end: /* @__PURE__ */ new Date(),
|
|
1272
|
+
limit: 10,
|
|
1273
|
+
sort: "desc",
|
|
1274
|
+
include_content: true
|
|
1275
|
+
};
|
|
1276
|
+
const mergedParams = { ...defaultParams, ...params };
|
|
1277
|
+
let newsArticles = [];
|
|
1278
|
+
let pageToken = null;
|
|
1279
|
+
let hasMorePages = true;
|
|
1280
|
+
let fetchedCount = 0;
|
|
1281
|
+
const maxLimit = mergedParams.limit;
|
|
1282
|
+
function cleanContent(content) {
|
|
1283
|
+
if (!content) return void 0;
|
|
1284
|
+
return content.replace(/\s+/g, " ").trim();
|
|
1285
|
+
}
|
|
1286
|
+
while (hasMorePages) {
|
|
1287
|
+
const queryParams = new URLSearchParams({
|
|
1288
|
+
...mergedParams.start && { start: new Date(mergedParams.start).toISOString() },
|
|
1289
|
+
...mergedParams.end && { end: new Date(mergedParams.end).toISOString() },
|
|
1290
|
+
...symbol && { symbols: symbol },
|
|
1291
|
+
...mergedParams.limit && { limit: Math.min(50, maxLimit - fetchedCount).toString() },
|
|
1292
|
+
...mergedParams.sort && { sort: mergedParams.sort },
|
|
1293
|
+
...mergedParams.include_content !== void 0 ? { include_content: mergedParams.include_content.toString() } : {},
|
|
1294
|
+
...pageToken && { page_token: pageToken }
|
|
1295
|
+
});
|
|
1296
|
+
const url = `${this.v1beta1url}/news?${queryParams}`;
|
|
1297
|
+
log2(`Fetching news from: ${url}`, { type: "debug", symbol });
|
|
1298
|
+
const response = await fetch(url, {
|
|
1299
|
+
method: "GET",
|
|
1300
|
+
headers: this.headers
|
|
1301
|
+
});
|
|
1302
|
+
if (!response.ok) {
|
|
1303
|
+
const errorText = await response.text();
|
|
1304
|
+
log2(`Alpaca news API error (${response.status}): ${errorText}`, { type: "error", symbol });
|
|
1305
|
+
throw new Error(`Alpaca news API error (${response.status}): ${errorText}`);
|
|
1306
|
+
}
|
|
1307
|
+
const data = await response.json();
|
|
1308
|
+
if (!data.news || !Array.isArray(data.news)) {
|
|
1309
|
+
log2(`No news data found in Alpaca response for ${symbol}`, { type: "warn", symbol });
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
const transformedNews = data.news.map((article) => ({
|
|
1313
|
+
symbols: article.symbols,
|
|
1314
|
+
title: article.headline,
|
|
1315
|
+
summary: cleanContent(article.summary) ?? "",
|
|
1316
|
+
content: article.content ? cleanContent(article.content) : void 0,
|
|
1317
|
+
url: article.url,
|
|
1318
|
+
source: article.source,
|
|
1319
|
+
author: article.author,
|
|
1320
|
+
date: article.updated_at || article.created_at,
|
|
1321
|
+
updatedDate: article.updated_at || article.created_at,
|
|
1322
|
+
sentiment: 0
|
|
1323
|
+
}));
|
|
1324
|
+
newsArticles = newsArticles.concat(transformedNews);
|
|
1325
|
+
fetchedCount = newsArticles.length;
|
|
1326
|
+
pageToken = data.next_page_token || null;
|
|
1327
|
+
hasMorePages = !!pageToken && (!maxLimit || fetchedCount < maxLimit);
|
|
1328
|
+
log2(
|
|
1329
|
+
`Fetched ${transformedNews.length} news articles (total: ${fetchedCount}) for ${symbol}. More pages: ${hasMorePages}`,
|
|
1330
|
+
{ type: "debug", symbol }
|
|
1331
|
+
);
|
|
1332
|
+
if (maxLimit && fetchedCount >= maxLimit) {
|
|
1333
|
+
newsArticles = newsArticles.slice(0, maxLimit);
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return newsArticles;
|
|
1338
|
+
}
|
|
1339
|
+
// ===== CRYPTO MARKET DATA METHODS =====
|
|
1340
|
+
/**
|
|
1341
|
+
* Get historical OHLCV bars for crypto symbols
|
|
1342
|
+
* Automatically handles pagination to fetch all available data
|
|
1343
|
+
* @param params Parameters for crypto historical bars request
|
|
1344
|
+
* @returns Historical bars data with all pages combined
|
|
1345
|
+
*/
|
|
1346
|
+
async getCryptoHistoricalBars(params) {
|
|
1347
|
+
const symbols = params.symbols;
|
|
1348
|
+
const symbolsStr = symbols.join(",");
|
|
1349
|
+
let allBars = {};
|
|
1350
|
+
let pageToken = null;
|
|
1351
|
+
let hasMorePages = true;
|
|
1352
|
+
let totalBarsCount = 0;
|
|
1353
|
+
let pageCount = 0;
|
|
1354
|
+
symbols.forEach((symbol) => {
|
|
1355
|
+
allBars[symbol] = [];
|
|
1356
|
+
});
|
|
1357
|
+
log2(
|
|
1358
|
+
`Starting crypto historical bars fetch for ${symbols.length} symbols (${params.timeframe}, ${params.start || "no start"} to ${params.end || "no end"})`
|
|
1359
|
+
);
|
|
1360
|
+
while (hasMorePages) {
|
|
1361
|
+
pageCount++;
|
|
1362
|
+
const requestParams = {
|
|
1363
|
+
...params,
|
|
1364
|
+
symbols: symbolsStr,
|
|
1365
|
+
...pageToken && { page_token: pageToken }
|
|
1366
|
+
};
|
|
1367
|
+
const response = await this.makeRequest(
|
|
1368
|
+
"/crypto/us/bars",
|
|
1369
|
+
"GET",
|
|
1370
|
+
requestParams,
|
|
1371
|
+
"v1beta3"
|
|
1372
|
+
);
|
|
1373
|
+
if (!response.bars) {
|
|
1374
|
+
log2(`No crypto bars data found in response for ${symbols.length} symbols`, { type: "warn" });
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
let pageBarsCount = 0;
|
|
1378
|
+
Object.entries(response.bars).forEach(([symbol, bars]) => {
|
|
1379
|
+
if (bars && bars.length > 0) {
|
|
1380
|
+
allBars[symbol] = [...allBars[symbol], ...bars];
|
|
1381
|
+
pageBarsCount += bars.length;
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
totalBarsCount += pageBarsCount;
|
|
1385
|
+
pageToken = response.next_page_token || null;
|
|
1386
|
+
hasMorePages = !!pageToken;
|
|
1387
|
+
log2(
|
|
1388
|
+
`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} crypto bars (total: ${totalBarsCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ", more pages available" : ", complete"}`,
|
|
1389
|
+
{ type: "debug" }
|
|
1390
|
+
);
|
|
1391
|
+
if (pageCount > 1e3) {
|
|
1392
|
+
log2(`Stopping crypto bars pagination after ${pageCount} pages to prevent infinite loop`, { type: "warn" });
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
log2(
|
|
1397
|
+
`Crypto historical bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages`
|
|
1398
|
+
);
|
|
1399
|
+
return {
|
|
1400
|
+
bars: allBars,
|
|
1401
|
+
next_page_token: null
|
|
1402
|
+
// Always null since we fetch all pages
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Get the most recent minute bar for requested crypto symbols
|
|
1407
|
+
* @param symbols Array of crypto symbols to query
|
|
1408
|
+
* @returns Latest bar data for each symbol
|
|
1409
|
+
*/
|
|
1410
|
+
async getCryptoLatestBars(symbols) {
|
|
1411
|
+
return this.makeRequest("/crypto/us/latest/bars", "GET", { symbols: symbols.join(",") }, "v1beta3");
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Get historical quotes for crypto symbols
|
|
1415
|
+
* Automatically handles pagination to fetch all available data
|
|
1416
|
+
* @param params Parameters for crypto historical quotes request
|
|
1417
|
+
* @returns Historical quotes data with all pages combined
|
|
1418
|
+
*/
|
|
1419
|
+
async getCryptoHistoricalQuotes(params) {
|
|
1420
|
+
const symbols = params.symbols;
|
|
1421
|
+
const symbolsStr = symbols.join(",");
|
|
1422
|
+
let allQuotes = {};
|
|
1423
|
+
let pageToken = null;
|
|
1424
|
+
let hasMorePages = true;
|
|
1425
|
+
let totalQuotesCount = 0;
|
|
1426
|
+
let pageCount = 0;
|
|
1427
|
+
symbols.forEach((symbol) => {
|
|
1428
|
+
allQuotes[symbol] = [];
|
|
1429
|
+
});
|
|
1430
|
+
log2(
|
|
1431
|
+
`Starting crypto historical quotes fetch for ${symbols.length} symbols (${params.start || "no start"} to ${params.end || "no end"})`
|
|
1432
|
+
);
|
|
1433
|
+
while (hasMorePages) {
|
|
1434
|
+
pageCount++;
|
|
1435
|
+
const requestParams = {
|
|
1436
|
+
...params,
|
|
1437
|
+
symbols: symbolsStr,
|
|
1438
|
+
...pageToken && { page_token: pageToken }
|
|
1439
|
+
};
|
|
1440
|
+
const response = await this.makeRequest(
|
|
1441
|
+
"/crypto/us/quotes",
|
|
1442
|
+
"GET",
|
|
1443
|
+
requestParams,
|
|
1444
|
+
"v1beta3"
|
|
1445
|
+
);
|
|
1446
|
+
if (!response.quotes) {
|
|
1447
|
+
log2(`No crypto quotes data found in response for ${symbols.length} symbols`, { type: "warn" });
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
let pageQuotesCount = 0;
|
|
1451
|
+
Object.entries(response.quotes).forEach(([symbol, quotes]) => {
|
|
1452
|
+
if (quotes && quotes.length > 0) {
|
|
1453
|
+
allQuotes[symbol] = [...allQuotes[symbol], ...quotes];
|
|
1454
|
+
pageQuotesCount += quotes.length;
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
totalQuotesCount += pageQuotesCount;
|
|
1458
|
+
pageToken = response.next_page_token || null;
|
|
1459
|
+
hasMorePages = !!pageToken;
|
|
1460
|
+
log2(
|
|
1461
|
+
`Page ${pageCount}: Fetched ${pageQuotesCount.toLocaleString()} crypto quotes (total: ${totalQuotesCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ", more pages available" : ", complete"}`,
|
|
1462
|
+
{ type: "debug" }
|
|
1463
|
+
);
|
|
1464
|
+
if (pageCount > 1e3) {
|
|
1465
|
+
log2(`Stopping crypto quotes pagination after ${pageCount} pages to prevent infinite loop`, { type: "warn" });
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
log2(
|
|
1470
|
+
`Crypto historical quotes fetch complete: ${totalQuotesCount.toLocaleString()} total quotes across ${pageCount} pages`
|
|
1471
|
+
);
|
|
1472
|
+
return {
|
|
1473
|
+
quotes: allQuotes,
|
|
1474
|
+
next_page_token: null
|
|
1475
|
+
// Always null since we fetch all pages
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Get the most recent quotes for requested crypto symbols
|
|
1480
|
+
* @param symbols Array of crypto symbols to query
|
|
1481
|
+
* @returns Latest quote data for each symbol
|
|
1482
|
+
*/
|
|
1483
|
+
async getCryptoLatestQuotes(symbols) {
|
|
1484
|
+
if (!symbols || symbols.length === 0) {
|
|
1485
|
+
log2("No symbols provided to getCryptoLatestQuotes, returning empty response", { type: "warn" });
|
|
1486
|
+
return { quotes: {} };
|
|
1487
|
+
}
|
|
1488
|
+
return this.makeRequest("/crypto/us/latest/quotes", "GET", { symbols: symbols.join(",") }, "v1beta3");
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Get historical trades for crypto symbols
|
|
1492
|
+
* Automatically handles pagination to fetch all available data
|
|
1493
|
+
* @param params Parameters for crypto historical trades request
|
|
1494
|
+
* @returns Historical trades data with all pages combined
|
|
1495
|
+
*/
|
|
1496
|
+
async getCryptoHistoricalTrades(params) {
|
|
1497
|
+
const symbols = params.symbols;
|
|
1498
|
+
const symbolsStr = symbols.join(",");
|
|
1499
|
+
let allTrades = {};
|
|
1500
|
+
let pageToken = null;
|
|
1501
|
+
let hasMorePages = true;
|
|
1502
|
+
let totalTradesCount = 0;
|
|
1503
|
+
let pageCount = 0;
|
|
1504
|
+
symbols.forEach((symbol) => {
|
|
1505
|
+
allTrades[symbol] = [];
|
|
1506
|
+
});
|
|
1507
|
+
log2(
|
|
1508
|
+
`Starting crypto historical trades fetch for ${symbols.length} symbols (${params.start || "no start"} to ${params.end || "no end"})`
|
|
1509
|
+
);
|
|
1510
|
+
while (hasMorePages) {
|
|
1511
|
+
pageCount++;
|
|
1512
|
+
const requestParams = {
|
|
1513
|
+
...params,
|
|
1514
|
+
symbols: symbolsStr,
|
|
1515
|
+
...pageToken && { page_token: pageToken }
|
|
1516
|
+
};
|
|
1517
|
+
const response = await this.makeRequest(
|
|
1518
|
+
"/crypto/us/trades",
|
|
1519
|
+
"GET",
|
|
1520
|
+
requestParams,
|
|
1521
|
+
"v1beta3"
|
|
1522
|
+
);
|
|
1523
|
+
if (!response.trades) {
|
|
1524
|
+
log2(`No crypto trades data found in response for ${symbols.length} symbols`, { type: "warn" });
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
let pageTradesCount = 0;
|
|
1528
|
+
Object.entries(response.trades).forEach(([symbol, trades]) => {
|
|
1529
|
+
if (trades && trades.length > 0) {
|
|
1530
|
+
allTrades[symbol] = [...allTrades[symbol], ...trades];
|
|
1531
|
+
pageTradesCount += trades.length;
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
totalTradesCount += pageTradesCount;
|
|
1535
|
+
pageToken = response.next_page_token || null;
|
|
1536
|
+
hasMorePages = !!pageToken;
|
|
1537
|
+
log2(
|
|
1538
|
+
`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} crypto trades (total: ${totalTradesCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ", more pages available" : ", complete"}`,
|
|
1539
|
+
{ type: "debug" }
|
|
1540
|
+
);
|
|
1541
|
+
if (pageCount > 1e3) {
|
|
1542
|
+
log2(`Stopping crypto trades pagination after ${pageCount} pages to prevent infinite loop`, { type: "warn" });
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
log2(
|
|
1547
|
+
`Crypto historical trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages`
|
|
1548
|
+
);
|
|
1549
|
+
return {
|
|
1550
|
+
trades: allTrades,
|
|
1551
|
+
next_page_token: null
|
|
1552
|
+
// Always null since we fetch all pages
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Get the most recent trades for requested crypto symbols
|
|
1557
|
+
* @param symbols Array of crypto symbols to query
|
|
1558
|
+
* @returns Latest trade data for each symbol
|
|
1559
|
+
*/
|
|
1560
|
+
async getCryptoLatestTrades(symbols) {
|
|
1561
|
+
if (!symbols || symbols.length === 0) {
|
|
1562
|
+
log2("No symbols provided to getCryptoLatestTrades, returning empty response", { type: "warn" });
|
|
1563
|
+
return { trades: {} };
|
|
1564
|
+
}
|
|
1565
|
+
return this.makeRequest("/crypto/us/latest/trades", "GET", { symbols: symbols.join(",") }, "v1beta3");
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Get snapshots for crypto symbols
|
|
1569
|
+
* Returns the latest trade, latest quote, latest minute bar, latest daily bar, and previous daily bar data
|
|
1570
|
+
* @param symbols Array of crypto symbols to query
|
|
1571
|
+
* @returns Snapshot data for each symbol
|
|
1572
|
+
*/
|
|
1573
|
+
async getCryptoSnapshots(symbols) {
|
|
1574
|
+
if (!symbols || symbols.length === 0) {
|
|
1575
|
+
log2("No symbols provided to getCryptoSnapshots, returning empty response", { type: "warn" });
|
|
1576
|
+
return { snapshots: {} };
|
|
1577
|
+
}
|
|
1578
|
+
return this.makeRequest("/crypto/us/snapshots", "GET", { symbols: symbols.join(",") }, "v1beta3");
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Get the latest orderbook for requested crypto symbols
|
|
1582
|
+
* @param symbols Array of crypto symbols to query
|
|
1583
|
+
* @returns Latest orderbook data for each symbol
|
|
1584
|
+
*/
|
|
1585
|
+
async getCryptoLatestOrderbooks(symbols) {
|
|
1586
|
+
if (!symbols || symbols.length === 0) {
|
|
1587
|
+
log2("No symbols provided to getCryptoLatestOrderbooks, returning empty response", { type: "warn" });
|
|
1588
|
+
return { orderbooks: {} };
|
|
1589
|
+
}
|
|
1590
|
+
return this.makeRequest("/crypto/us/latest/orderbooks", "GET", { symbols: symbols.join(",") }, "v1beta3");
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Analyzes an array of crypto bars and returns a summary string
|
|
1594
|
+
* @param bars Array of crypto bars to analyze
|
|
1595
|
+
* @returns A string summarizing the crypto price data
|
|
1596
|
+
*/
|
|
1597
|
+
static analyzeCryptoBars(bars) {
|
|
1598
|
+
if (!bars || bars.length === 0) {
|
|
1599
|
+
return "No crypto price data available";
|
|
1600
|
+
}
|
|
1601
|
+
const firstBar = bars[0];
|
|
1602
|
+
const lastBar = bars[bars.length - 1];
|
|
1603
|
+
const priceChange = lastBar.c - firstBar.o;
|
|
1604
|
+
const percentChange = priceChange / firstBar.o * 100;
|
|
1605
|
+
const volumeChange = lastBar.v - firstBar.v;
|
|
1606
|
+
const percentVolumeChange = firstBar.v > 0 ? volumeChange / firstBar.v * 100 : 0;
|
|
1607
|
+
const high = Math.max(...bars.map((bar) => bar.h));
|
|
1608
|
+
const low = Math.min(...bars.map((bar) => bar.l));
|
|
1609
|
+
const totalVolume = bars.reduce((sum, bar) => sum + bar.v, 0);
|
|
1610
|
+
const avgVolume = totalVolume / bars.length;
|
|
1611
|
+
return `Crypto Price: $${firstBar.o.toFixed(6)} -> $${lastBar.c.toFixed(6)} (${percentChange.toFixed(2)}%), Volume: ${firstBar.v.toLocaleString()} -> ${lastBar.v.toLocaleString()} (${percentVolumeChange.toFixed(2)}%), High: $${high.toFixed(6)}, Low: $${low.toFixed(6)}, Avg Volume: ${avgVolume.toLocaleString()}`;
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
marketDataAPI = AlpacaMarketDataAPI.getInstance();
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
// src/alpaca-trading-api.ts
|
|
1619
|
+
import WebSocket2 from "ws";
|
|
1620
|
+
var init_alpaca_trading_api = __esm({
|
|
1621
|
+
"src/alpaca-trading-api.ts"() {
|
|
1622
|
+
"use strict";
|
|
1623
|
+
init_logging();
|
|
1624
|
+
init_alpaca_market_data_api();
|
|
1625
|
+
init_market_time();
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
// src/index.ts
|
|
1630
|
+
init_market_time();
|
|
1631
|
+
|
|
1632
|
+
// src/types/llm-types.ts
|
|
1633
|
+
var OPENAI_MODELS = [
|
|
1634
|
+
"gpt-5.6",
|
|
1635
|
+
"gpt-5.6-sol",
|
|
1636
|
+
"gpt-5.6-terra",
|
|
1637
|
+
"gpt-5.6-luna",
|
|
1638
|
+
"gpt-5",
|
|
1639
|
+
"gpt-5-mini",
|
|
1640
|
+
"gpt-5.4-mini",
|
|
1641
|
+
"gpt-5.4-nano",
|
|
1642
|
+
"gpt-5-nano",
|
|
1643
|
+
"gpt-5.1",
|
|
1644
|
+
"gpt-5.4",
|
|
1645
|
+
"gpt-5.5",
|
|
1646
|
+
"gpt-5.5-pro",
|
|
1647
|
+
"gpt-5.2",
|
|
1648
|
+
"gpt-5.2-pro",
|
|
1649
|
+
"gpt-5.1-codex",
|
|
1650
|
+
"gpt-5.1-codex-max"
|
|
1651
|
+
];
|
|
1652
|
+
function isOpenAIModel(model) {
|
|
1653
|
+
return OPENAI_MODELS.includes(model);
|
|
1654
|
+
}
|
|
1655
|
+
var OPENROUTER_MODELS = [
|
|
1656
|
+
"openai/gpt-5",
|
|
1657
|
+
"openai/gpt-5-mini",
|
|
1658
|
+
"openai/gpt-5.4-mini",
|
|
1659
|
+
"openai/gpt-5.4-nano",
|
|
1660
|
+
"openai/gpt-5-nano",
|
|
1661
|
+
"openai/gpt-5.1",
|
|
1662
|
+
"openai/gpt-5.4",
|
|
1663
|
+
"openai/gpt-5.4-pro",
|
|
1664
|
+
"openai/gpt-5.5",
|
|
1665
|
+
"openai/gpt-5.5-pro",
|
|
1666
|
+
"openai/gpt-5.2",
|
|
1667
|
+
"openai/gpt-5.2-pro",
|
|
1668
|
+
"openai/gpt-5.1-codex",
|
|
1669
|
+
"openai/gpt-5.1-codex-max",
|
|
1670
|
+
"openai/gpt-oss-120b",
|
|
1671
|
+
"z.ai/glm-4.5",
|
|
1672
|
+
"z.ai/glm-4.5-air",
|
|
1673
|
+
"google/gemini-2.5-flash",
|
|
1674
|
+
"google/gemini-2.5-flash-lite",
|
|
1675
|
+
"deepseek/deepseek-r1-0528",
|
|
1676
|
+
"deepseek/deepseek-chat-v3-0324"
|
|
1677
|
+
];
|
|
1678
|
+
function isOpenRouterModel(model) {
|
|
1679
|
+
return OPENROUTER_MODELS.includes(model);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/llm-openai.ts
|
|
1683
|
+
import OpenAI2 from "openai";
|
|
1684
|
+
import { zodTextFormat } from "openai/helpers/zod";
|
|
1685
|
+
|
|
1686
|
+
// src/llm-config.ts
|
|
1687
|
+
var DEFAULT_MODEL = "gpt-5.6-luna";
|
|
1688
|
+
var GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS = 272e3;
|
|
1689
|
+
var GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER = 2;
|
|
1690
|
+
var GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER = 1.5;
|
|
1691
|
+
var openAiModelCosts = {
|
|
1692
|
+
"gpt-5.6": {
|
|
1693
|
+
inputCost: 5 / 1e6,
|
|
1694
|
+
cacheHitCost: 0.5 / 1e6,
|
|
1695
|
+
cacheWriteCost: 6.25 / 1e6,
|
|
1696
|
+
outputCost: 30 / 1e6
|
|
1697
|
+
},
|
|
1698
|
+
"gpt-5.6-sol": {
|
|
1699
|
+
inputCost: 5 / 1e6,
|
|
1700
|
+
cacheHitCost: 0.5 / 1e6,
|
|
1701
|
+
cacheWriteCost: 6.25 / 1e6,
|
|
1702
|
+
outputCost: 30 / 1e6
|
|
1703
|
+
},
|
|
1704
|
+
"gpt-5.6-terra": {
|
|
1705
|
+
inputCost: 2.5 / 1e6,
|
|
1706
|
+
cacheHitCost: 0.25 / 1e6,
|
|
1707
|
+
cacheWriteCost: 3.125 / 1e6,
|
|
1708
|
+
outputCost: 15 / 1e6
|
|
1709
|
+
},
|
|
1710
|
+
"gpt-5.6-luna": {
|
|
1711
|
+
inputCost: 1 / 1e6,
|
|
1712
|
+
cacheHitCost: 0.1 / 1e6,
|
|
1713
|
+
cacheWriteCost: 1.25 / 1e6,
|
|
1714
|
+
outputCost: 6 / 1e6
|
|
1715
|
+
},
|
|
1716
|
+
"gpt-5": {
|
|
1717
|
+
inputCost: 1.25 / 1e6,
|
|
1718
|
+
cacheHitCost: 0.125 / 1e6,
|
|
1719
|
+
outputCost: 10 / 1e6
|
|
1720
|
+
},
|
|
1721
|
+
"gpt-5-mini": {
|
|
1722
|
+
inputCost: 0.25 / 1e6,
|
|
1723
|
+
cacheHitCost: 0.025 / 1e6,
|
|
1724
|
+
outputCost: 2 / 1e6
|
|
1725
|
+
},
|
|
1726
|
+
"gpt-5.4-mini": {
|
|
1727
|
+
inputCost: 0.75 / 1e6,
|
|
1728
|
+
cacheHitCost: 0.075 / 1e6,
|
|
1729
|
+
outputCost: 4.5 / 1e6
|
|
1730
|
+
},
|
|
1731
|
+
"gpt-5.4-nano": {
|
|
1732
|
+
inputCost: 0.2 / 1e6,
|
|
1733
|
+
cacheHitCost: 0.02 / 1e6,
|
|
1734
|
+
outputCost: 1.25 / 1e6
|
|
1735
|
+
},
|
|
1736
|
+
"gpt-5-nano": {
|
|
1737
|
+
inputCost: 0.05 / 1e6,
|
|
1738
|
+
cacheHitCost: 5e-3 / 1e6,
|
|
1739
|
+
outputCost: 0.4 / 1e6
|
|
1740
|
+
},
|
|
1741
|
+
"gpt-5.1": {
|
|
1742
|
+
inputCost: 1.25 / 1e6,
|
|
1743
|
+
cacheHitCost: 0.125 / 1e6,
|
|
1744
|
+
outputCost: 10 / 1e6
|
|
1745
|
+
},
|
|
1746
|
+
"gpt-5.4": {
|
|
1747
|
+
inputCost: 2.5 / 1e6,
|
|
1748
|
+
cacheHitCost: 0.25 / 1e6,
|
|
1749
|
+
outputCost: 15 / 1e6
|
|
1750
|
+
},
|
|
1751
|
+
"gpt-5.5": {
|
|
1752
|
+
inputCost: 5 / 1e6,
|
|
1753
|
+
cacheHitCost: 0.5 / 1e6,
|
|
1754
|
+
outputCost: 30 / 1e6
|
|
1755
|
+
},
|
|
1756
|
+
"gpt-5.5-pro": {
|
|
1757
|
+
inputCost: 30 / 1e6,
|
|
1758
|
+
outputCost: 180 / 1e6
|
|
1759
|
+
},
|
|
1760
|
+
"gpt-5.2": {
|
|
1761
|
+
inputCost: 1.75 / 1e6,
|
|
1762
|
+
cacheHitCost: 0.175 / 1e6,
|
|
1763
|
+
outputCost: 14 / 1e6
|
|
1764
|
+
},
|
|
1765
|
+
"gpt-5.2-pro": {
|
|
1766
|
+
inputCost: 21 / 1e6,
|
|
1767
|
+
outputCost: 168 / 1e6
|
|
1768
|
+
},
|
|
1769
|
+
"gpt-5.1-codex": {
|
|
1770
|
+
inputCost: 1.25 / 1e6,
|
|
1771
|
+
cacheHitCost: 0.125 / 1e6,
|
|
1772
|
+
outputCost: 10 / 1e6
|
|
1773
|
+
},
|
|
1774
|
+
"gpt-5.1-codex-max": {
|
|
1775
|
+
inputCost: 1.25 / 1e6,
|
|
1776
|
+
cacheHitCost: 0.125 / 1e6,
|
|
1777
|
+
outputCost: 10 / 1e6
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
var deepseekModelCosts = {
|
|
1781
|
+
"deepseek-chat": {
|
|
1782
|
+
inputCost: 0.27 / 1e6,
|
|
1783
|
+
// $0.27 per 1M tokens (Cache miss price)
|
|
1784
|
+
cacheHitCost: 0.07 / 1e6,
|
|
1785
|
+
// $0.07 per 1M tokens (Cache hit price)
|
|
1786
|
+
outputCost: 1.1 / 1e6
|
|
1787
|
+
// $1.10 per 1M tokens
|
|
1788
|
+
},
|
|
1789
|
+
"deepseek-reasoner": {
|
|
1790
|
+
inputCost: 0.55 / 1e6,
|
|
1791
|
+
// $0.55 per 1M tokens (Cache miss price)
|
|
1792
|
+
cacheHitCost: 0.14 / 1e6,
|
|
1793
|
+
// $0.14 per 1M tokens (Cache hit price)
|
|
1794
|
+
outputCost: 2.19 / 1e6
|
|
1795
|
+
// $2.19 per 1M tokens
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
function shouldUseGPT5HighContextPricing(model, inputTokens) {
|
|
1799
|
+
return (model === "gpt-5.4" || model === "gpt-5.5" || model === "gpt-5.6" || model === "gpt-5.6-sol" || model === "gpt-5.6-terra" || model === "gpt-5.6-luna") && inputTokens > GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS;
|
|
1800
|
+
}
|
|
1801
|
+
var DEFAULT_IMAGE_PRICING_QUALITY = "high";
|
|
1802
|
+
var DEFAULT_IMAGE_PRICING_SIZE = "1024x1024";
|
|
1803
|
+
var openAiImageCostByQualityAndSize = {
|
|
1804
|
+
"gpt-image-2": {
|
|
1805
|
+
low: {
|
|
1806
|
+
"1024x1024": 6e-3,
|
|
1807
|
+
"1024x1536": 5e-3,
|
|
1808
|
+
"1536x1024": 5e-3
|
|
1809
|
+
},
|
|
1810
|
+
medium: {
|
|
1811
|
+
"1024x1024": 0.053,
|
|
1812
|
+
"1024x1536": 0.041,
|
|
1813
|
+
"1536x1024": 0.041
|
|
1814
|
+
},
|
|
1815
|
+
high: {
|
|
1816
|
+
"1024x1024": 0.211,
|
|
1817
|
+
"1024x1536": 0.165,
|
|
1818
|
+
"1536x1024": 0.165
|
|
1819
|
+
}
|
|
1820
|
+
},
|
|
1821
|
+
"gpt-image-2-2026-04-21": {
|
|
1822
|
+
low: {
|
|
1823
|
+
"1024x1024": 6e-3,
|
|
1824
|
+
"1024x1536": 5e-3,
|
|
1825
|
+
"1536x1024": 5e-3
|
|
1826
|
+
},
|
|
1827
|
+
medium: {
|
|
1828
|
+
"1024x1024": 0.053,
|
|
1829
|
+
"1024x1536": 0.041,
|
|
1830
|
+
"1536x1024": 0.041
|
|
1831
|
+
},
|
|
1832
|
+
high: {
|
|
1833
|
+
"1024x1024": 0.211,
|
|
1834
|
+
"1024x1536": 0.165,
|
|
1835
|
+
"1536x1024": 0.165
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
"gpt-image-1.5": {
|
|
1839
|
+
low: {
|
|
1840
|
+
"1024x1024": 9e-3,
|
|
1841
|
+
"1024x1536": 0.013,
|
|
1842
|
+
"1536x1024": 0.013
|
|
1843
|
+
},
|
|
1844
|
+
medium: {
|
|
1845
|
+
"1024x1024": 0.034,
|
|
1846
|
+
"1024x1536": 0.05,
|
|
1847
|
+
"1536x1024": 0.05
|
|
1848
|
+
},
|
|
1849
|
+
high: {
|
|
1850
|
+
"1024x1024": 0.133,
|
|
1851
|
+
"1024x1536": 0.2,
|
|
1852
|
+
"1536x1024": 0.2
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
"gpt-image-1": {
|
|
1856
|
+
low: {
|
|
1857
|
+
"1024x1024": 0.011,
|
|
1858
|
+
"1024x1536": 0.016,
|
|
1859
|
+
"1536x1024": 0.016
|
|
1860
|
+
},
|
|
1861
|
+
medium: {
|
|
1862
|
+
"1024x1024": 0.042,
|
|
1863
|
+
"1024x1536": 0.063,
|
|
1864
|
+
"1536x1024": 0.063
|
|
1865
|
+
},
|
|
1866
|
+
high: {
|
|
1867
|
+
"1024x1024": 0.167,
|
|
1868
|
+
"1024x1536": 0.25,
|
|
1869
|
+
"1536x1024": 0.25
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
"gpt-image-1-mini": {
|
|
1873
|
+
low: {
|
|
1874
|
+
"1024x1024": 5e-3,
|
|
1875
|
+
"1024x1536": 6e-3,
|
|
1876
|
+
"1536x1024": 6e-3
|
|
1877
|
+
},
|
|
1878
|
+
medium: {
|
|
1879
|
+
"1024x1024": 0.011,
|
|
1880
|
+
"1024x1536": 0.015,
|
|
1881
|
+
"1536x1024": 0.015
|
|
1882
|
+
},
|
|
1883
|
+
high: {
|
|
1884
|
+
"1024x1024": 0.036,
|
|
1885
|
+
"1024x1536": 0.052,
|
|
1886
|
+
"1536x1024": 0.052
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
var openAiImageCosts = {
|
|
1891
|
+
"gpt-image-2": openAiImageCostByQualityAndSize["gpt-image-2"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
1892
|
+
"gpt-image-2-2026-04-21": openAiImageCostByQualityAndSize["gpt-image-2-2026-04-21"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
1893
|
+
"gpt-image-1.5": openAiImageCostByQualityAndSize["gpt-image-1.5"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
1894
|
+
"gpt-image-1": openAiImageCostByQualityAndSize["gpt-image-1"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
1895
|
+
"gpt-image-1-mini": openAiImageCostByQualityAndSize["gpt-image-1-mini"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE]
|
|
1896
|
+
};
|
|
1897
|
+
function calculateCost(provider, model, inputTokens, outputTokens, reasoningTokens, cacheHitTokens, cacheWriteTokens) {
|
|
1898
|
+
if (typeof provider !== "string" || typeof model !== "string" || typeof inputTokens !== "number" || typeof outputTokens !== "number" || reasoningTokens !== void 0 && typeof reasoningTokens !== "number" || cacheHitTokens !== void 0 && typeof cacheHitTokens !== "number" || cacheWriteTokens !== void 0 && typeof cacheWriteTokens !== "number") {
|
|
1899
|
+
return 0;
|
|
1900
|
+
}
|
|
1901
|
+
const modelCosts = provider === "deepseek" ? deepseekModelCosts[model] : openAiModelCosts[model];
|
|
1902
|
+
if (!modelCosts) return 0;
|
|
1903
|
+
const boundedCacheHitTokens = Math.min(Math.max(cacheHitTokens || 0, 0), inputTokens);
|
|
1904
|
+
const boundedCacheWriteTokens = Math.min(Math.max(cacheWriteTokens || 0, 0), inputTokens - boundedCacheHitTokens);
|
|
1905
|
+
const uncachedInputTokens = inputTokens - boundedCacheHitTokens - boundedCacheWriteTokens;
|
|
1906
|
+
const inputCost = uncachedInputTokens * modelCosts.inputCost + boundedCacheHitTokens * (modelCosts.cacheHitCost || modelCosts.inputCost) + boundedCacheWriteTokens * (modelCosts.cacheWriteCost || modelCosts.inputCost);
|
|
1907
|
+
let outputCost = outputTokens * modelCosts.outputCost;
|
|
1908
|
+
let reasoningCost = (reasoningTokens || 0) * modelCosts.outputCost;
|
|
1909
|
+
if (provider === "openai" && shouldUseGPT5HighContextPricing(model, inputTokens)) {
|
|
1910
|
+
return inputCost * GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER + outputCost * GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER + reasoningCost * GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER;
|
|
1911
|
+
}
|
|
1912
|
+
return inputCost + outputCost + reasoningCost;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/json-tools.ts
|
|
1916
|
+
import OpenAI from "openai";
|
|
1917
|
+
|
|
1918
|
+
// src/llm-images.ts
|
|
1919
|
+
import OpenAI3 from "openai";
|
|
1920
|
+
var MULTIMODAL_VISION_MODELS = new Set(OPENAI_MODELS);
|
|
1921
|
+
|
|
1922
|
+
// src/llm-deepseek.ts
|
|
1923
|
+
import OpenAI4 from "openai";
|
|
1924
|
+
|
|
1925
|
+
// src/llm-openrouter.ts
|
|
1926
|
+
import OpenAI5 from "openai";
|
|
1927
|
+
|
|
1928
|
+
// src/index.ts
|
|
1929
|
+
init_alpaca_trading_api();
|
|
1930
|
+
init_alpaca_market_data_api();
|
|
1931
|
+
init_alpaca_trading_api();
|
|
1932
|
+
init_alpaca_market_data_api();
|
|
1933
|
+
|
|
1934
|
+
// src/test.ts
|
|
1935
|
+
init_logging();
|
|
1936
|
+
init_alpaca_market_data_api();
|
|
1937
|
+
init_alpaca_trading_api();
|
|
1938
|
+
import { z } from "zod";
|
|
1939
|
+
var DEBUG_LOGGING2 = process.env["DEBUG"] === "true" || false;
|
|
1940
|
+
var log3 = (message, options = { type: "info" }) => {
|
|
1941
|
+
if (!DEBUG_LOGGING2 && options.type === "debug") {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
log(message, { ...options, source: "Test" });
|
|
1945
|
+
};
|
|
1946
|
+
function assertNear(actual, expected, label) {
|
|
1947
|
+
const tolerance = 1e-12;
|
|
1948
|
+
if (Math.abs(actual - expected) > tolerance) {
|
|
1949
|
+
throw new Error(`${label} expected ${expected}, received ${actual}`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function testOpenAIModelRegistryAndPricing() {
|
|
1953
|
+
const directModel = "gpt-5.6-terra";
|
|
1954
|
+
const openRouterModel = "openai/gpt-5.4-nano";
|
|
1955
|
+
const gpt56Models = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
|
|
1956
|
+
for (const model of gpt56Models) {
|
|
1957
|
+
if (!isOpenAIModel(model) || !OPENAI_MODELS.includes(model)) {
|
|
1958
|
+
throw new Error(`${model} is missing from direct OpenAI model exports`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (!isOpenRouterModel(openRouterModel) || !OPENROUTER_MODELS.includes(openRouterModel)) {
|
|
1962
|
+
throw new Error(`${openRouterModel} is missing from OpenRouter model exports`);
|
|
1963
|
+
}
|
|
1964
|
+
assertNear(calculateCost("openai", directModel, 1e5, 0), 0.25, "gpt-5.6-terra input pricing");
|
|
1965
|
+
assertNear(calculateCost("openai", directModel, 1e5, 0, 0, 1e5), 0.025, "gpt-5.6-terra cached input pricing");
|
|
1966
|
+
assertNear(calculateCost("openai", directModel, 0, 1e6), 15, "gpt-5.6-terra output pricing");
|
|
1967
|
+
assertNear(calculateCost("openai", directModel, 1e5, 0, 0, 0, 1e5), 0.3125, "gpt-5.6-terra cache-write pricing");
|
|
1968
|
+
assertNear(calculateCost("openai", "gpt-5.6-luna", 1e5, 1e5), 0.7, "gpt-5.6-luna pricing");
|
|
1969
|
+
assertNear(calculateCost("openai", directModel, 1e6, 0), 5, "gpt-5.6-terra long-context input pricing");
|
|
1970
|
+
assertNear(calculateCost("openai", "gpt-5.4-mini", 1e6, 1e6), 5.25, "gpt-5.4-mini pricing");
|
|
1971
|
+
if (DEFAULT_MODEL !== "gpt-5.6-luna") {
|
|
1972
|
+
throw new Error(`Expected gpt-5.6-luna as the default model, received ${DEFAULT_MODEL}`);
|
|
1973
|
+
}
|
|
1974
|
+
log3(`${directModel} registry and pricing test passed`);
|
|
1975
|
+
}
|
|
1976
|
+
testOpenAIModelRegistryAndPricing();
|
|
1977
|
+
//# sourceMappingURL=test.mjs.map
|