@alphafox/cli 0.3.21 → 0.3.23
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/auth/refresh.js +3 -4
- package/dist/commands/run.js +2 -2
- package/dist/engine-backtest/fetch-runtime.d.ts +4 -0
- package/dist/engine-backtest/fetch-runtime.js +103 -17
- package/dist/engine-backtest/parse-args.js +10 -2
- package/dist/engine-backtest/run-command.js +4 -1
- package/dist/engine-backtest/sweep-command.js +2 -2
- package/dist/engine-backtest/types.d.ts +5 -1
- package/dist/http/client.js +3 -3
- package/dist/skills-manifest.json +45 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/agents/domain.md +3 -49
- package/docs/agents/issue-tracker.md +5 -41
- package/docs/agents/triage-labels.md +2 -14
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +6 -28
- package/skills/alphafox-shared/SKILL.md +6 -6
- package/skills/auth/SKILL.md +1 -1
- package/skills/cache/SKILL.md +3 -3
- package/skills/engine-backtest/SKILL.md +3 -3
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +2 -2
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +6 -4
- package/skills/trading/SKILL.md +2 -2
- package/vendor/backtest-runner/README.md +2 -0
- package/vendor/backtest-runner/index.d.ts +8 -2
- package/vendor/backtest-runner/index.mjs +5 -2
- package/vendor/backtest-runner/lib/series.mjs +5 -171
- package/vendor/backtest-runner/lib/tape-loader-concurrency.mjs +72 -0
- package/vendor/backtest-runner/lib/tape-loader-range-plan.mjs +77 -0
- package/vendor/backtest-runner/lib/tape-loader-range.mjs +222 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +13 -41
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const DEFAULT_TAPE_SERIES_CONCURRENCY = 8;
|
|
2
|
+
export const MAX_TAPE_SERIES_CONCURRENCY = 8;
|
|
3
|
+
|
|
4
|
+
export function resolveTapeSeriesConcurrency(value) {
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 1) {
|
|
6
|
+
return Math.min(
|
|
7
|
+
MAX_TAPE_SERIES_CONCURRENCY,
|
|
8
|
+
Math.max(1, Math.floor(value))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return DEFAULT_TAPE_SERIES_CONCURRENCY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function mapWithConcurrency(items, concurrency, worker) {
|
|
15
|
+
const list = [...items];
|
|
16
|
+
if (list.length === 0) return [];
|
|
17
|
+
|
|
18
|
+
const limit = Math.min(
|
|
19
|
+
list.length,
|
|
20
|
+
Math.max(1, Math.floor(Number(concurrency)) || 1)
|
|
21
|
+
);
|
|
22
|
+
const results = new Array(list.length);
|
|
23
|
+
let nextIndex = 0;
|
|
24
|
+
async function runWorker() {
|
|
25
|
+
while (true) {
|
|
26
|
+
const index = nextIndex;
|
|
27
|
+
nextIndex += 1;
|
|
28
|
+
if (index >= list.length) return;
|
|
29
|
+
results[index] = await worker(list[index], index);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Shares one request budget across series workers and their time windows. */
|
|
37
|
+
export function limitTapeOhlcvConcurrency(exchange, concurrency, signal) {
|
|
38
|
+
const limit = resolveTapeSeriesConcurrency(concurrency);
|
|
39
|
+
const queue = [];
|
|
40
|
+
let active = 0;
|
|
41
|
+
|
|
42
|
+
const drain = () => {
|
|
43
|
+
while (active < limit) {
|
|
44
|
+
const task = queue.shift();
|
|
45
|
+
if (!task) return;
|
|
46
|
+
active += 1;
|
|
47
|
+
void task().finally(() => {
|
|
48
|
+
active -= 1;
|
|
49
|
+
drain();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const schedule = (operation) =>
|
|
54
|
+
new Promise((resolve, reject) => {
|
|
55
|
+
queue.push(async () => {
|
|
56
|
+
try {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
resolve(await operation());
|
|
59
|
+
} catch (error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
drain();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
fetchOHLCV: (symbol, timeframe, since, pageLimit, params) =>
|
|
68
|
+
schedule(() =>
|
|
69
|
+
exchange.fetchOHLCV(symbol, timeframe, since, pageLimit, params)
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { TapeDataUnavailableError } from "./coverage.mjs";
|
|
2
|
+
import { MAX_TAPE_SERIES_CONCURRENCY } from "./tape-loader-concurrency.mjs";
|
|
3
|
+
|
|
4
|
+
export function planOhlcvTimeSegments(request, stepMs, pageLimit) {
|
|
5
|
+
const candleCount = Math.max(
|
|
6
|
+
0,
|
|
7
|
+
Math.ceil((request.toMs - request.sinceMs) / stepMs)
|
|
8
|
+
);
|
|
9
|
+
const segmentCount = Math.min(
|
|
10
|
+
MAX_TAPE_SERIES_CONCURRENCY,
|
|
11
|
+
Math.max(1, Math.ceil(candleCount / pageLimit))
|
|
12
|
+
);
|
|
13
|
+
if (segmentCount === 1) {
|
|
14
|
+
return [{ sinceMs: request.sinceMs, untilMs: request.toMs }];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const candlesPerSegment = Math.ceil(candleCount / segmentCount);
|
|
18
|
+
return Array.from({ length: segmentCount }, (_, index) => {
|
|
19
|
+
const logicalStartMs = request.sinceMs + index * candlesPerSegment * stepMs;
|
|
20
|
+
return {
|
|
21
|
+
// Overlap preserves bars whose exchange open time is not split-aligned.
|
|
22
|
+
sinceMs:
|
|
23
|
+
index === 0
|
|
24
|
+
? request.sinceMs
|
|
25
|
+
: Math.max(request.sinceMs, logicalStartMs - stepMs),
|
|
26
|
+
untilMs: Math.min(
|
|
27
|
+
request.toMs,
|
|
28
|
+
request.sinceMs + (index + 1) * candlesPerSegment * stepMs
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
}).filter((segment) => segment.untilMs > segment.sinceMs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSegmentRequest(request, segment, onFraction) {
|
|
35
|
+
return {
|
|
36
|
+
...request,
|
|
37
|
+
sinceMs: segment.sinceMs,
|
|
38
|
+
toMs: segment.untilMs,
|
|
39
|
+
fromMs: Math.min(
|
|
40
|
+
segment.untilMs,
|
|
41
|
+
Math.max(segment.sinceMs, request.fromMs)
|
|
42
|
+
),
|
|
43
|
+
allowEmpty: true,
|
|
44
|
+
onFraction,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mergeOhlcvTimeSegments(request, segments) {
|
|
49
|
+
const rowsByTimestamp = new Map();
|
|
50
|
+
for (const rows of segments) {
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
if (row[0] < request.sinceMs) continue;
|
|
53
|
+
const existing = rowsByTimestamp.get(row[0]);
|
|
54
|
+
if (existing && !sameOhlcvRow(existing, row)) {
|
|
55
|
+
throw new TapeDataUnavailableError([
|
|
56
|
+
{
|
|
57
|
+
code: "invalid_ohlcv",
|
|
58
|
+
symbol: request.symbol,
|
|
59
|
+
timeframe: request.timeframe,
|
|
60
|
+
timestamp: row[0],
|
|
61
|
+
message: "concurrent OHLCV windows returned conflicting candles",
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
rowsByTimestamp.set(row[0], row);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...rowsByTimestamp.values()].sort((left, right) => left[0] - right[0]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function averageProgress(values) {
|
|
72
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sameOhlcvRow(left, right) {
|
|
76
|
+
return left.every((value, index) => value === right[index]);
|
|
77
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { abortable } from "./abortable.mjs";
|
|
2
|
+
import { TapeDataUnavailableError } from "./coverage.mjs";
|
|
3
|
+
import {
|
|
4
|
+
averageProgress,
|
|
5
|
+
createSegmentRequest,
|
|
6
|
+
mergeOhlcvTimeSegments,
|
|
7
|
+
planOhlcvTimeSegments,
|
|
8
|
+
} from "./tape-loader-range-plan.mjs";
|
|
9
|
+
import { TIMEFRAME_MS } from "./timeframes.mjs";
|
|
10
|
+
|
|
11
|
+
const BITGET_OHLCV_MAX_REQUEST_SPAN_MS = 89 * 86_400_000;
|
|
12
|
+
|
|
13
|
+
export function isClosedCandle(timestampMs, timeframe, toMs) {
|
|
14
|
+
return timestampMs + timeframeStepMs(timeframe) <= toMs;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function fetchClosedOhlcvRange(request) {
|
|
18
|
+
const stepMs = timeframeStepMs(request.timeframe);
|
|
19
|
+
const pageLimit = ohlcvPageLimitForTimeframe(
|
|
20
|
+
request.exchangeDefinition,
|
|
21
|
+
request.runtimeConfig,
|
|
22
|
+
request.timeframe
|
|
23
|
+
);
|
|
24
|
+
const segments = planOhlcvTimeSegments(request, stepMs, pageLimit);
|
|
25
|
+
if (segments.length === 1) {
|
|
26
|
+
return fetchClosedOhlcvTimeSegment(request, pageLimit);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const progress = new Array(segments.length).fill(0);
|
|
30
|
+
const segmentRows = await Promise.all(
|
|
31
|
+
segments.map((segment, index) =>
|
|
32
|
+
fetchClosedOhlcvTimeSegment(
|
|
33
|
+
createSegmentRequest(request, segment, (fraction) => {
|
|
34
|
+
progress[index] = fraction;
|
|
35
|
+
request.onFraction?.(averageProgress(progress));
|
|
36
|
+
}),
|
|
37
|
+
pageLimit
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
const rows = mergeOhlcvTimeSegments(request, segmentRows);
|
|
42
|
+
if (rows.length === 0 && !request.allowEmpty) {
|
|
43
|
+
throw missingOhlcvError(request);
|
|
44
|
+
}
|
|
45
|
+
request.onFraction?.(1);
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchClosedOhlcvTimeSegment(request, pageLimit) {
|
|
50
|
+
const stepMs = timeframeStepMs(request.timeframe);
|
|
51
|
+
const rows = [];
|
|
52
|
+
const totalSpan = Math.max(1, request.toMs - request.sinceMs);
|
|
53
|
+
let cursor = request.sinceMs;
|
|
54
|
+
let emptyPrefixLowerMs = null;
|
|
55
|
+
let nonEmptyPrefixUpperMs = null;
|
|
56
|
+
let prefixResolved = false;
|
|
57
|
+
|
|
58
|
+
while (cursor < request.toMs) {
|
|
59
|
+
request.signal?.throwIfAborted();
|
|
60
|
+
const fetchSinceMs = resolveFetchSinceMs(request, cursor);
|
|
61
|
+
const page = await fetchOhlcvPage(request, fetchSinceMs, pageLimit, stepMs);
|
|
62
|
+
request.signal?.throwIfAborted();
|
|
63
|
+
|
|
64
|
+
const prefix = resolveEmptyPrefix({
|
|
65
|
+
request,
|
|
66
|
+
page,
|
|
67
|
+
rows,
|
|
68
|
+
cursor,
|
|
69
|
+
stepMs,
|
|
70
|
+
emptyPrefixLowerMs,
|
|
71
|
+
nonEmptyPrefixUpperMs,
|
|
72
|
+
prefixResolved,
|
|
73
|
+
});
|
|
74
|
+
if (prefix) {
|
|
75
|
+
({ cursor, emptyPrefixLowerMs, nonEmptyPrefixUpperMs, prefixResolved } =
|
|
76
|
+
prefix);
|
|
77
|
+
if (prefix.done) break;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (page.length === 0) break;
|
|
81
|
+
prefixResolved = true;
|
|
82
|
+
appendPageRows(request, rows, page, stepMs);
|
|
83
|
+
const nextCursor = Number(page[page.length - 1][0]) + stepMs;
|
|
84
|
+
if (nextCursor <= cursor) break;
|
|
85
|
+
cursor = nextCursor;
|
|
86
|
+
request.onFraction?.(Math.min(1, (cursor - request.sinceMs) / totalSpan));
|
|
87
|
+
}
|
|
88
|
+
if (rows.length === 0 && !request.allowEmpty) {
|
|
89
|
+
throw missingOhlcvError(request);
|
|
90
|
+
}
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveEmptyPrefix(input) {
|
|
95
|
+
if (input.prefixResolved || input.rows.length > 0) return null;
|
|
96
|
+
if (input.page.length === 0 && input.cursor >= input.request.fromMs) {
|
|
97
|
+
return { ...input, done: true };
|
|
98
|
+
}
|
|
99
|
+
if (input.page.length > 0 && input.emptyPrefixLowerMs === null) return null;
|
|
100
|
+
|
|
101
|
+
const emptyPrefixLowerMs =
|
|
102
|
+
input.page.length === 0 ? input.cursor : input.emptyPrefixLowerMs;
|
|
103
|
+
const nonEmptyPrefixUpperMs =
|
|
104
|
+
input.page.length > 0 ? input.cursor : input.nonEmptyPrefixUpperMs;
|
|
105
|
+
const upperMs = nonEmptyPrefixUpperMs ?? input.request.fromMs;
|
|
106
|
+
const prefixResolved =
|
|
107
|
+
nonEmptyPrefixUpperMs !== null &&
|
|
108
|
+
upperMs - (emptyPrefixLowerMs ?? upperMs) <= input.stepMs;
|
|
109
|
+
return {
|
|
110
|
+
cursor: prefixResolved
|
|
111
|
+
? upperMs
|
|
112
|
+
: midpointCursorMs(
|
|
113
|
+
emptyPrefixLowerMs ?? input.cursor,
|
|
114
|
+
upperMs,
|
|
115
|
+
input.stepMs
|
|
116
|
+
),
|
|
117
|
+
emptyPrefixLowerMs,
|
|
118
|
+
nonEmptyPrefixUpperMs,
|
|
119
|
+
prefixResolved,
|
|
120
|
+
done: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function fetchOhlcvPage(request, fetchSinceMs, pageLimit, stepMs) {
|
|
125
|
+
const params = { ...request.runtimeConfig.requestParams };
|
|
126
|
+
if (request.exchangeDefinition.ccxtId === "hyperliquid") {
|
|
127
|
+
params.until = Math.min(
|
|
128
|
+
request.toMs,
|
|
129
|
+
fetchSinceMs + (pageLimit - 1) * stepMs
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return abortable(
|
|
133
|
+
request.exchange.fetchOHLCV(
|
|
134
|
+
request.symbol,
|
|
135
|
+
request.timeframe,
|
|
136
|
+
fetchSinceMs,
|
|
137
|
+
pageLimit,
|
|
138
|
+
params
|
|
139
|
+
),
|
|
140
|
+
request.signal
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendPageRows(request, rows, page, stepMs) {
|
|
145
|
+
for (const raw of page) {
|
|
146
|
+
const timestamp = Number(raw[0]);
|
|
147
|
+
if (raw.length < 6 || !Number.isFinite(timestamp)) {
|
|
148
|
+
throw invalidOhlcvError(request);
|
|
149
|
+
}
|
|
150
|
+
if (!isClosedCandle(timestamp, request.timeframe, request.toMs)) continue;
|
|
151
|
+
if (rows.length > 0 && timestamp <= rows[rows.length - 1][0]) {
|
|
152
|
+
throw new TapeDataUnavailableError([
|
|
153
|
+
{
|
|
154
|
+
code: "non_monotonic",
|
|
155
|
+
symbol: request.symbol,
|
|
156
|
+
timeframe: request.timeframe,
|
|
157
|
+
expected: rows[rows.length - 1][0] + stepMs,
|
|
158
|
+
actual: timestamp,
|
|
159
|
+
timestamp,
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
rows.push([
|
|
164
|
+
timestamp,
|
|
165
|
+
Number(raw[1]),
|
|
166
|
+
Number(raw[2]),
|
|
167
|
+
Number(raw[3]),
|
|
168
|
+
Number(raw[4]),
|
|
169
|
+
Number(raw[5]),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveFetchSinceMs(request, cursor) {
|
|
175
|
+
return request.exchangeDefinition.ccxtId === "bitget"
|
|
176
|
+
? Math.max(0, cursor - 1)
|
|
177
|
+
: cursor;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function ohlcvPageLimitForTimeframe(exchange, runtimeConfig, timeframe) {
|
|
181
|
+
if (exchange.ccxtId !== "bitget") return runtimeConfig.ohlcvPageLimit;
|
|
182
|
+
return Math.max(
|
|
183
|
+
1,
|
|
184
|
+
Math.min(
|
|
185
|
+
runtimeConfig.ohlcvPageLimit,
|
|
186
|
+
Math.floor(BITGET_OHLCV_MAX_REQUEST_SPAN_MS / timeframeStepMs(timeframe))
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function timeframeStepMs(timeframe) {
|
|
192
|
+
const stepMs = TIMEFRAME_MS[timeframe];
|
|
193
|
+
if (!stepMs) throw new Error(`不支持的 timeframe:${timeframe}`);
|
|
194
|
+
return stepMs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function midpointCursorMs(lowerMs, upperMs, stepMs) {
|
|
198
|
+
if (upperMs - lowerMs <= stepMs) return upperMs;
|
|
199
|
+
const midpointMs = lowerMs + Math.floor((upperMs - lowerMs) / 2);
|
|
200
|
+
const alignedMs = Math.floor(midpointMs / stepMs) * stepMs;
|
|
201
|
+
return Math.min(upperMs, Math.max(lowerMs + 1, alignedMs));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function invalidOhlcvError(request) {
|
|
205
|
+
return new TapeDataUnavailableError([
|
|
206
|
+
{
|
|
207
|
+
code: "invalid_ohlcv",
|
|
208
|
+
symbol: request.symbol,
|
|
209
|
+
timeframe: request.timeframe,
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function missingOhlcvError(request) {
|
|
215
|
+
return new TapeDataUnavailableError([
|
|
216
|
+
{
|
|
217
|
+
code: "ohlcv_missing",
|
|
218
|
+
symbol: request.symbol,
|
|
219
|
+
timeframe: request.timeframe,
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
@@ -19,9 +19,15 @@ import {
|
|
|
19
19
|
buildCcxtConstructorOptions,
|
|
20
20
|
ccxtExchangeClientCacheKey,
|
|
21
21
|
} from "./proxy.mjs";
|
|
22
|
-
import { loadSeriesWithCache
|
|
22
|
+
import { loadSeriesWithCache } from "./series.mjs";
|
|
23
|
+
import {
|
|
24
|
+
limitTapeOhlcvConcurrency,
|
|
25
|
+
mapWithConcurrency,
|
|
26
|
+
resolveTapeSeriesConcurrency,
|
|
27
|
+
} from "./tape-loader-concurrency.mjs";
|
|
23
28
|
import {
|
|
24
29
|
ENGINE_BACKTEST_BASE_TIMEFRAME,
|
|
30
|
+
TIMEFRAME_MS,
|
|
25
31
|
baseTimeframeStepMs,
|
|
26
32
|
resolvePlanBaseTimeframe,
|
|
27
33
|
} from "./timeframes.mjs";
|
|
@@ -40,47 +46,8 @@ const FUNDING_INTERVALS = [
|
|
|
40
46
|
{ interval: "8h", spacingMs: 28_800_000 },
|
|
41
47
|
];
|
|
42
48
|
|
|
43
|
-
/** Independent symbol×timeframe (and funding) fetches. Pagination stays serial. */
|
|
44
|
-
export const DEFAULT_TAPE_SERIES_CONCURRENCY = 4;
|
|
45
|
-
export const MAX_TAPE_SERIES_CONCURRENCY = 8;
|
|
46
|
-
|
|
47
49
|
const exchangePromises = new Map();
|
|
48
50
|
|
|
49
|
-
export function resolveTapeSeriesConcurrency(value) {
|
|
50
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 1) {
|
|
51
|
-
return Math.min(
|
|
52
|
-
MAX_TAPE_SERIES_CONCURRENCY,
|
|
53
|
-
Math.max(1, Math.floor(value))
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return DEFAULT_TAPE_SERIES_CONCURRENCY;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function mapWithConcurrency(items, concurrency, worker) {
|
|
60
|
-
const list = [...items];
|
|
61
|
-
if (list.length === 0) {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
const limit = Math.min(
|
|
65
|
-
list.length,
|
|
66
|
-
Math.max(1, Math.floor(Number(concurrency)) || 1)
|
|
67
|
-
);
|
|
68
|
-
const results = new Array(list.length);
|
|
69
|
-
let nextIndex = 0;
|
|
70
|
-
async function runWorker() {
|
|
71
|
-
while (true) {
|
|
72
|
-
const index = nextIndex;
|
|
73
|
-
nextIndex += 1;
|
|
74
|
-
if (index >= list.length) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
results[index] = await worker(list[index], index);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
81
|
-
return results;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
51
|
export function effectiveTapeEndMs(
|
|
85
52
|
requestedToMs,
|
|
86
53
|
nowMs = Date.now(),
|
|
@@ -287,6 +254,11 @@ export async function loadTape(request, options = {}) {
|
|
|
287
254
|
const dataIssues = [];
|
|
288
255
|
const coverageWarnings = [];
|
|
289
256
|
const coverageIssues = [];
|
|
257
|
+
const ohlcvExchange = limitTapeOhlcvConcurrency(
|
|
258
|
+
exchange,
|
|
259
|
+
seriesConcurrency,
|
|
260
|
+
request.signal
|
|
261
|
+
);
|
|
290
262
|
const seriesFractions = new Array(totalSeries).fill(0);
|
|
291
263
|
let lastOhlcvDetail = "";
|
|
292
264
|
const reportOhlcv = (index, fraction, detail) => {
|
|
@@ -311,7 +283,7 @@ export async function loadTape(request, options = {}) {
|
|
|
311
283
|
const detail = `${symbol} ${timeframe}`;
|
|
312
284
|
try {
|
|
313
285
|
const loaded = await loadSeriesWithCache(
|
|
314
|
-
|
|
286
|
+
ohlcvExchange,
|
|
315
287
|
exchangeDefinition,
|
|
316
288
|
runtimeConfig,
|
|
317
289
|
symbol,
|