tradelab 0.4.0 → 1.0.0
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/README.md +121 -52
- package/bin/tradelab.js +340 -49
- package/dist/cjs/data.cjs +210 -155
- package/dist/cjs/index.cjs +1782 -274
- package/dist/cjs/live.cjs +3350 -0
- package/docs/README.md +26 -9
- package/docs/api-reference.md +89 -26
- package/docs/backtest-engine.md +74 -60
- package/docs/data-reporting-cli.md +66 -36
- package/docs/examples.md +275 -0
- package/docs/live-trading.md +186 -0
- package/examples/yahooEmaCross.js +1 -6
- package/package.json +18 -3
- package/src/data/csv.js +24 -14
- package/src/data/index.js +1 -5
- package/src/data/yahoo.js +6 -19
- package/src/engine/backtest.js +137 -144
- package/src/engine/backtestTicks.js +481 -0
- package/src/engine/barSystemRunner.js +1027 -0
- package/src/engine/execution.js +11 -39
- package/src/engine/portfolio.js +237 -66
- package/src/engine/walkForward.js +132 -13
- package/src/index.js +3 -11
- package/src/live/broker/alpaca.js +254 -0
- package/src/live/broker/binance.js +351 -0
- package/src/live/broker/coinbase.js +339 -0
- package/src/live/broker/interactiveBrokers.js +123 -0
- package/src/live/broker/interface.js +74 -0
- package/src/live/clock.js +56 -0
- package/src/live/engine/candleAggregator.js +154 -0
- package/src/live/engine/liveEngine.js +694 -0
- package/src/live/engine/paperEngine.js +453 -0
- package/src/live/engine/riskManager.js +185 -0
- package/src/live/engine/stateManager.js +112 -0
- package/src/live/events.js +48 -0
- package/src/live/feed/brokerFeed.js +35 -0
- package/src/live/feed/interface.js +28 -0
- package/src/live/feed/pollingFeed.js +105 -0
- package/src/live/index.js +27 -0
- package/src/live/logger.js +82 -0
- package/src/live/orchestrator.js +133 -0
- package/src/live/storage/interface.js +36 -0
- package/src/live/storage/jsonFileStorage.js +112 -0
- package/src/metrics/buildMetrics.js +103 -100
- package/src/reporting/exportBacktestArtifacts.js +1 -4
- package/src/reporting/exportTradesCsv.js +2 -7
- package/src/reporting/renderHtmlReport.js +8 -13
- package/src/utils/indicators.js +1 -2
- package/src/utils/positionSizing.js +16 -2
- package/src/utils/time.js +4 -12
- package/templates/report.html +23 -9
- package/templates/report.js +83 -69
- package/types/index.d.ts +98 -4
- package/types/live.d.ts +382 -0
|
@@ -19,6 +19,73 @@ function stitchEquitySeries(target, source) {
|
|
|
19
19
|
target.push(...nextPoints);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function canonicalParams(params) {
|
|
23
|
+
const entries = Object.entries(params || {}).sort(([left], [right]) => left.localeCompare(right));
|
|
24
|
+
return JSON.stringify(Object.fromEntries(entries));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function describeValue(value) {
|
|
28
|
+
if (Array.isArray(value)) return `array(length=${value.length})`;
|
|
29
|
+
if (value === null) return "null";
|
|
30
|
+
return typeof value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildWindowRanges(length, trainBars, testBars, stepBars, mode) {
|
|
34
|
+
const ranges = [];
|
|
35
|
+
for (let start = 0; start + trainBars + testBars <= length; start += stepBars) {
|
|
36
|
+
const trainStart = mode === "anchored" ? 0 : start;
|
|
37
|
+
const trainEnd = mode === "anchored" ? trainBars + start : start + trainBars;
|
|
38
|
+
const testStart = trainEnd;
|
|
39
|
+
const testEnd = testStart + testBars;
|
|
40
|
+
if (testEnd > length) break;
|
|
41
|
+
ranges.push({ trainStart, trainEnd, testStart, testEnd });
|
|
42
|
+
}
|
|
43
|
+
return ranges;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function summarizeBestParams(windows) {
|
|
47
|
+
const summaryBySignature = new Map();
|
|
48
|
+
let adjacentRepeats = 0;
|
|
49
|
+
|
|
50
|
+
windows.forEach((window, index) => {
|
|
51
|
+
const signature = window.bestParamsSignature ?? canonicalParams(window.bestParams);
|
|
52
|
+
const current = summaryBySignature.get(signature) || {
|
|
53
|
+
params: window.bestParams,
|
|
54
|
+
wins: 0,
|
|
55
|
+
profitableWindows: 0,
|
|
56
|
+
oosTrades: 0,
|
|
57
|
+
};
|
|
58
|
+
current.wins += 1;
|
|
59
|
+
current.profitableWindows += window.profitable ? 1 : 0;
|
|
60
|
+
current.oosTrades += window.oosTrades;
|
|
61
|
+
summaryBySignature.set(signature, current);
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
index > 0 &&
|
|
65
|
+
(windows[index - 1].bestParamsSignature ?? canonicalParams(windows[index - 1].bestParams)) ===
|
|
66
|
+
signature
|
|
67
|
+
) {
|
|
68
|
+
adjacentRepeats += 1;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const byFrequency = [...summaryBySignature.values()].sort((left, right) => {
|
|
73
|
+
if (right.wins !== left.wins) return right.wins - left.wins;
|
|
74
|
+
return right.profitableWindows - left.profitableWindows;
|
|
75
|
+
});
|
|
76
|
+
const adjacentPairs = Math.max(0, windows.length - 1);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
winners: windows.map((window) => window.bestParams),
|
|
80
|
+
stability: {
|
|
81
|
+
adjacentRepeatRate: adjacentPairs ? adjacentRepeats / adjacentPairs : 0,
|
|
82
|
+
uniqueWinnerCount: summaryBySignature.size,
|
|
83
|
+
dominant: byFrequency[0] || null,
|
|
84
|
+
leaderboard: byFrequency,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
22
89
|
/**
|
|
23
90
|
* Run rolling walk-forward optimization over a single candle series.
|
|
24
91
|
*
|
|
@@ -32,40 +99,66 @@ export function walkForwardOptimize({
|
|
|
32
99
|
trainBars,
|
|
33
100
|
testBars,
|
|
34
101
|
stepBars = testBars,
|
|
102
|
+
mode = "rolling",
|
|
35
103
|
scoreBy = "profitFactor",
|
|
36
104
|
backtestOptions = {},
|
|
37
105
|
} = {}) {
|
|
38
106
|
if (!Array.isArray(candles) || candles.length === 0) {
|
|
39
|
-
throw new Error(
|
|
107
|
+
throw new Error(
|
|
108
|
+
`walkForwardOptimize() requires a non-empty candles array, got ${describeValue(candles)}`
|
|
109
|
+
);
|
|
40
110
|
}
|
|
41
111
|
if (typeof signalFactory !== "function") {
|
|
42
|
-
throw new Error(
|
|
112
|
+
throw new Error(
|
|
113
|
+
`walkForwardOptimize() requires a signalFactory function, got ${describeValue(signalFactory)}`
|
|
114
|
+
);
|
|
43
115
|
}
|
|
44
116
|
if (!Array.isArray(parameterSets) || parameterSets.length === 0) {
|
|
45
|
-
throw new Error(
|
|
117
|
+
throw new Error(
|
|
118
|
+
`walkForwardOptimize() requires parameterSets, got ${describeValue(parameterSets)}`
|
|
119
|
+
);
|
|
46
120
|
}
|
|
47
121
|
if (!(trainBars > 0) || !(testBars > 0) || !(stepBars > 0)) {
|
|
48
122
|
throw new Error("walkForwardOptimize() requires positive trainBars, testBars, and stepBars");
|
|
49
123
|
}
|
|
124
|
+
if (mode !== "rolling" && mode !== "anchored") {
|
|
125
|
+
throw new Error('walkForwardOptimize() mode must be "rolling" or "anchored"');
|
|
126
|
+
}
|
|
50
127
|
|
|
51
128
|
const windows = [];
|
|
52
129
|
const allTrades = [];
|
|
53
130
|
const allPositions = [];
|
|
54
131
|
const eqSeries = [];
|
|
55
132
|
let rollingEquity = backtestOptions.equity ?? 10_000;
|
|
133
|
+
const ranges = buildWindowRanges(candles.length, trainBars, testBars, stepBars, mode);
|
|
134
|
+
if (!ranges.length) {
|
|
135
|
+
const required = trainBars + testBars;
|
|
136
|
+
throw new Error(
|
|
137
|
+
`walkForwardOptimize() produced zero windows: need at least ${required} candles (trainBars=${trainBars} + testBars=${testBars}) but got ${candles.length}. Try reducing trainBars/testBars or adding more historical data.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const trainBacktestOptions = {
|
|
141
|
+
...backtestOptions,
|
|
142
|
+
collectEqSeries: false,
|
|
143
|
+
collectReplay: false,
|
|
144
|
+
};
|
|
145
|
+
const testBacktestOptions = { ...backtestOptions };
|
|
56
146
|
|
|
57
|
-
for (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
147
|
+
for (const range of ranges) {
|
|
148
|
+
const trainSlice = candles.slice(range.trainStart, range.trainEnd);
|
|
149
|
+
const testSlice = candles.slice(range.testStart, range.testEnd);
|
|
150
|
+
if (!trainSlice.length || !testSlice.length) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`walkForwardOptimize() generated an empty window (train=${trainSlice.length}, test=${testSlice.length}, range=${JSON.stringify(
|
|
153
|
+
range
|
|
154
|
+
)})`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
64
157
|
|
|
65
158
|
let best = null;
|
|
66
159
|
for (const params of parameterSets) {
|
|
67
160
|
const trainResult = backtest({
|
|
68
|
-
...
|
|
161
|
+
...trainBacktestOptions,
|
|
69
162
|
candles: trainSlice,
|
|
70
163
|
equity: rollingEquity,
|
|
71
164
|
signal: signalFactory(params),
|
|
@@ -77,11 +170,12 @@ export function walkForwardOptimize({
|
|
|
77
170
|
}
|
|
78
171
|
|
|
79
172
|
const testResult = backtest({
|
|
80
|
-
...
|
|
173
|
+
...testBacktestOptions,
|
|
81
174
|
candles: testSlice,
|
|
82
175
|
equity: rollingEquity,
|
|
83
176
|
signal: signalFactory(best.params),
|
|
84
177
|
});
|
|
178
|
+
const bestParamsSignature = canonicalParams(best.params);
|
|
85
179
|
|
|
86
180
|
rollingEquity = testResult.metrics.finalEquity;
|
|
87
181
|
allTrades.push(...testResult.trades);
|
|
@@ -101,10 +195,29 @@ export function walkForwardOptimize({
|
|
|
101
195
|
trainScore: best.score,
|
|
102
196
|
trainMetrics: best.metrics,
|
|
103
197
|
testMetrics: testResult.metrics,
|
|
198
|
+
oosTrades: testResult.metrics.trades,
|
|
199
|
+
profitable: testResult.metrics.totalPnL > 0,
|
|
200
|
+
stabilityScore: 0,
|
|
201
|
+
bestParamsSignature,
|
|
104
202
|
result: testResult,
|
|
105
203
|
});
|
|
106
204
|
}
|
|
107
205
|
|
|
206
|
+
for (let index = 0; index < windows.length; index += 1) {
|
|
207
|
+
const currentSignature = windows[index].bestParamsSignature;
|
|
208
|
+
const adjacent = [];
|
|
209
|
+
if (index > 0) {
|
|
210
|
+
adjacent.push(windows[index - 1].bestParamsSignature === currentSignature ? 1 : 0);
|
|
211
|
+
}
|
|
212
|
+
if (index + 1 < windows.length) {
|
|
213
|
+
adjacent.push(windows[index + 1].bestParamsSignature === currentSignature ? 1 : 0);
|
|
214
|
+
}
|
|
215
|
+
windows[index].stabilityScore = adjacent.length
|
|
216
|
+
? adjacent.reduce((total, value) => total + value, 0) / adjacent.length
|
|
217
|
+
: 1;
|
|
218
|
+
delete windows[index].bestParamsSignature;
|
|
219
|
+
}
|
|
220
|
+
|
|
108
221
|
const metrics = buildMetrics({
|
|
109
222
|
closed: allTrades,
|
|
110
223
|
equityStart: backtestOptions.equity ?? 10_000,
|
|
@@ -113,14 +226,20 @@ export function walkForwardOptimize({
|
|
|
113
226
|
estBarMs: estimateBarMs(candles),
|
|
114
227
|
eqSeries,
|
|
115
228
|
});
|
|
229
|
+
const bestParamsSummary = summarizeBestParams(windows);
|
|
116
230
|
|
|
117
231
|
return {
|
|
118
232
|
windows,
|
|
119
233
|
trades: allTrades,
|
|
120
234
|
positions: allPositions,
|
|
235
|
+
openPositions: [],
|
|
121
236
|
metrics,
|
|
122
237
|
eqSeries,
|
|
123
238
|
replay: { frames: [], events: [] },
|
|
124
|
-
bestParams:
|
|
239
|
+
bestParams: Object.assign(
|
|
240
|
+
windows.map((window) => window.bestParams),
|
|
241
|
+
bestParamsSummary
|
|
242
|
+
),
|
|
243
|
+
bestParamsSummary: bestParamsSummary.stability,
|
|
125
244
|
};
|
|
126
245
|
}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { backtest } from "./engine/backtest.js";
|
|
2
|
+
export { backtestTicks } from "./engine/backtestTicks.js";
|
|
2
3
|
export { backtestPortfolio } from "./engine/portfolio.js";
|
|
3
4
|
export { walkForwardOptimize } from "./engine/walkForward.js";
|
|
4
5
|
|
|
@@ -17,10 +18,7 @@ export {
|
|
|
17
18
|
saveCandlesToCache,
|
|
18
19
|
} from "./data/index.js";
|
|
19
20
|
|
|
20
|
-
export {
|
|
21
|
-
renderHtmlReport,
|
|
22
|
-
exportHtmlReport,
|
|
23
|
-
} from "./reporting/renderHtmlReport.js";
|
|
21
|
+
export { renderHtmlReport, exportHtmlReport } from "./reporting/renderHtmlReport.js";
|
|
24
22
|
export { exportTradesCsv } from "./reporting/exportTradesCsv.js";
|
|
25
23
|
export { exportMetricsJSON } from "./reporting/exportMetricsJson.js";
|
|
26
24
|
export { exportBacktestArtifacts } from "./reporting/exportBacktestArtifacts.js";
|
|
@@ -37,10 +35,4 @@ export {
|
|
|
37
35
|
pct,
|
|
38
36
|
} from "./utils/indicators.js";
|
|
39
37
|
export { calculatePositionSize } from "./utils/positionSizing.js";
|
|
40
|
-
export {
|
|
41
|
-
offsetET,
|
|
42
|
-
minutesET,
|
|
43
|
-
isSession,
|
|
44
|
-
parseWindowsCSV,
|
|
45
|
-
inWindowsET,
|
|
46
|
-
} from "./utils/time.js";
|
|
38
|
+
export { offsetET, minutesET, isSession, parseWindowsCSV, inWindowsET } from "./utils/time.js";
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { URL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { normalizeCandles } from "../../data/csv.js";
|
|
4
|
+
import { BrokerAdapter } from "./interface.js";
|
|
5
|
+
|
|
6
|
+
function withQuery(url, query = {}) {
|
|
7
|
+
const target = new URL(url);
|
|
8
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9
|
+
if (value === undefined || value === null) continue;
|
|
10
|
+
target.searchParams.set(key, String(value));
|
|
11
|
+
}
|
|
12
|
+
return target.toString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mapOrderStatus(status) {
|
|
16
|
+
const normalized = String(status || "").toLowerCase();
|
|
17
|
+
if (normalized === "partially_filled") return "partially_filled";
|
|
18
|
+
if (normalized === "filled") return "filled";
|
|
19
|
+
if (normalized === "canceled" || normalized === "cancelled") return "canceled";
|
|
20
|
+
if (normalized === "rejected") return "rejected";
|
|
21
|
+
if (normalized === "expired") return "expired";
|
|
22
|
+
return "new";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mapOrderReceipt(order) {
|
|
26
|
+
return {
|
|
27
|
+
orderId: String(order.id),
|
|
28
|
+
clientOrderId: order.client_order_id,
|
|
29
|
+
status: mapOrderStatus(order.status),
|
|
30
|
+
filledQty: Number(order.filled_qty || 0),
|
|
31
|
+
avgFillPrice: Number.isFinite(Number(order.filled_avg_price))
|
|
32
|
+
? Number(order.filled_avg_price)
|
|
33
|
+
: undefined,
|
|
34
|
+
filledAt: order.filled_at ? Date.parse(order.filled_at) : undefined,
|
|
35
|
+
symbol: order.symbol,
|
|
36
|
+
side: order.side,
|
|
37
|
+
type: String(order.type || "").toLowerCase(),
|
|
38
|
+
qty: Number(order.qty || 0),
|
|
39
|
+
rejectReason: order.reject_reason,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Alpaca Markets broker adapter.
|
|
45
|
+
*/
|
|
46
|
+
export class AlpacaBroker extends BrokerAdapter {
|
|
47
|
+
constructor({ fetchImpl = globalThis.fetch } = {}) {
|
|
48
|
+
super();
|
|
49
|
+
this.fetch = fetchImpl;
|
|
50
|
+
this.connected = false;
|
|
51
|
+
this.config = {};
|
|
52
|
+
this.subscriptions = {
|
|
53
|
+
bars: new Map(),
|
|
54
|
+
quotes: new Map(),
|
|
55
|
+
trades: new Map(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async connect(config = {}) {
|
|
60
|
+
this.config = { ...config };
|
|
61
|
+
this.baseUrl =
|
|
62
|
+
config.baseUrl ||
|
|
63
|
+
(config.paper ? "https://paper-api.alpaca.markets" : "https://api.alpaca.markets");
|
|
64
|
+
this.dataUrl = config.dataUrl || "https://data.alpaca.markets";
|
|
65
|
+
this.connected = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async disconnect() {
|
|
69
|
+
this.connected = false;
|
|
70
|
+
this.subscriptions.bars.clear();
|
|
71
|
+
this.subscriptions.quotes.clear();
|
|
72
|
+
this.subscriptions.trades.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isConnected() {
|
|
76
|
+
return this.connected;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
supportsPaperNative() {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_headers(extra = {}) {
|
|
84
|
+
return {
|
|
85
|
+
"content-type": "application/json",
|
|
86
|
+
"APCA-API-KEY-ID": this.config.apiKey || "",
|
|
87
|
+
"APCA-API-SECRET-KEY": this.config.apiSecret || "",
|
|
88
|
+
...extra,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async _request(method, path, { query = null, body = null, dataApi = false } = {}) {
|
|
93
|
+
if (!this.fetch) throw new Error("global fetch is unavailable");
|
|
94
|
+
const base = dataApi ? this.dataUrl : this.baseUrl;
|
|
95
|
+
const url = withQuery(`${base}${path}`, query || {});
|
|
96
|
+
const response = await this.fetch(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers: this._headers(),
|
|
99
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
100
|
+
});
|
|
101
|
+
const text = await response.text();
|
|
102
|
+
const payload = text ? JSON.parse(text) : {};
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const message =
|
|
105
|
+
payload?.message || payload?.error || `alpaca request failed (${response.status})`;
|
|
106
|
+
throw new Error(message);
|
|
107
|
+
}
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getAccount() {
|
|
112
|
+
const account = await this._request("GET", "/v2/account");
|
|
113
|
+
return {
|
|
114
|
+
equity: Number(account.equity || 0),
|
|
115
|
+
buyingPower: Number(account.buying_power || 0),
|
|
116
|
+
cash: Number(account.cash || 0),
|
|
117
|
+
currency: account.currency || "USD",
|
|
118
|
+
marginUsed: Number(account.initial_margin || 0),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getPositions() {
|
|
123
|
+
const positions = await this._request("GET", "/v2/positions");
|
|
124
|
+
return positions.map((position) => ({
|
|
125
|
+
symbol: position.symbol,
|
|
126
|
+
side: String(position.side || "long").toLowerCase(),
|
|
127
|
+
qty: Number(position.qty || 0),
|
|
128
|
+
avgEntry: Number(position.avg_entry_price || 0),
|
|
129
|
+
marketValue: Number(position.market_value || 0),
|
|
130
|
+
unrealizedPnl: Number(position.unrealized_pl || 0),
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getServerTime() {
|
|
135
|
+
const clock = await this._request("GET", "/v2/clock");
|
|
136
|
+
return clock.timestamp ? Date.parse(clock.timestamp) : Date.now();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async submitOrder(order) {
|
|
140
|
+
const payload = {
|
|
141
|
+
symbol: order.symbol,
|
|
142
|
+
side: order.side,
|
|
143
|
+
type: order.type,
|
|
144
|
+
qty: String(order.qty),
|
|
145
|
+
time_in_force: order.timeInForce || "day",
|
|
146
|
+
client_order_id: order.clientOrderId,
|
|
147
|
+
};
|
|
148
|
+
if (order.limitPrice !== undefined) payload.limit_price = String(order.limitPrice);
|
|
149
|
+
if (order.stopPrice !== undefined) payload.stop_price = String(order.stopPrice);
|
|
150
|
+
const response = await this._request("POST", "/v2/orders", { body: payload });
|
|
151
|
+
const receipt = mapOrderReceipt(response);
|
|
152
|
+
this.emit("order:submitted", receipt);
|
|
153
|
+
return receipt;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async cancelOrder(orderId) {
|
|
157
|
+
await this._request("DELETE", `/v2/orders/${orderId}`);
|
|
158
|
+
this.emit("order:canceled", { orderId });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async modifyOrder(orderId, changes) {
|
|
162
|
+
const payload = {};
|
|
163
|
+
if (changes.qty !== undefined) payload.qty = String(changes.qty);
|
|
164
|
+
if (changes.limitPrice !== undefined) payload.limit_price = String(changes.limitPrice);
|
|
165
|
+
if (changes.stopPrice !== undefined) payload.stop_price = String(changes.stopPrice);
|
|
166
|
+
const response = await this._request("PATCH", `/v2/orders/${orderId}`, { body: payload });
|
|
167
|
+
const receipt = mapOrderReceipt(response);
|
|
168
|
+
this.emit("order:modified", receipt);
|
|
169
|
+
return receipt;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getOpenOrders() {
|
|
173
|
+
const orders = await this._request("GET", "/v2/orders", { query: { status: "open" } });
|
|
174
|
+
return orders.map(mapOrderReceipt);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async getOrderStatus(orderId) {
|
|
178
|
+
const order = await this._request("GET", `/v2/orders/${orderId}`);
|
|
179
|
+
return mapOrderReceipt(order);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async subscribeQuotes(symbol, handler) {
|
|
183
|
+
const key = symbol;
|
|
184
|
+
const list = this.subscriptions.quotes.get(key) || [];
|
|
185
|
+
list.push(handler);
|
|
186
|
+
this.subscriptions.quotes.set(key, list);
|
|
187
|
+
return {
|
|
188
|
+
unsubscribe: () => {
|
|
189
|
+
const current = this.subscriptions.quotes.get(key) || [];
|
|
190
|
+
this.subscriptions.quotes.set(
|
|
191
|
+
key,
|
|
192
|
+
current.filter((candidate) => candidate !== handler)
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async subscribeTrades(symbol, handler) {
|
|
199
|
+
const key = symbol;
|
|
200
|
+
const list = this.subscriptions.trades.get(key) || [];
|
|
201
|
+
list.push(handler);
|
|
202
|
+
this.subscriptions.trades.set(key, list);
|
|
203
|
+
return {
|
|
204
|
+
unsubscribe: () => {
|
|
205
|
+
const current = this.subscriptions.trades.get(key) || [];
|
|
206
|
+
this.subscriptions.trades.set(
|
|
207
|
+
key,
|
|
208
|
+
current.filter((candidate) => candidate !== handler)
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async subscribeBars(symbol, interval, handler) {
|
|
215
|
+
const key = `${symbol}::${interval}`;
|
|
216
|
+
const list = this.subscriptions.bars.get(key) || [];
|
|
217
|
+
list.push(handler);
|
|
218
|
+
this.subscriptions.bars.set(key, list);
|
|
219
|
+
return {
|
|
220
|
+
unsubscribe: () => {
|
|
221
|
+
const current = this.subscriptions.bars.get(key) || [];
|
|
222
|
+
this.subscriptions.bars.set(
|
|
223
|
+
key,
|
|
224
|
+
current.filter((candidate) => candidate !== handler)
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async getHistoricalBars(symbol, interval, limit = 200) {
|
|
231
|
+
const response = await this._request("GET", `/v2/stocks/${symbol}/bars`, {
|
|
232
|
+
dataApi: true,
|
|
233
|
+
query: {
|
|
234
|
+
timeframe: interval,
|
|
235
|
+
limit,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const bars = Array.isArray(response?.bars)
|
|
239
|
+
? response.bars.map((bar) => ({
|
|
240
|
+
time: Date.parse(bar.t),
|
|
241
|
+
open: Number(bar.o),
|
|
242
|
+
high: Number(bar.h),
|
|
243
|
+
low: Number(bar.l),
|
|
244
|
+
close: Number(bar.c),
|
|
245
|
+
volume: Number(bar.v ?? 0),
|
|
246
|
+
}))
|
|
247
|
+
: [];
|
|
248
|
+
return normalizeCandles(bars);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function createAlpacaBroker(options) {
|
|
253
|
+
return new AlpacaBroker(options);
|
|
254
|
+
}
|