@alphafox/cli 0.1.5 → 0.3.2
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 +42 -4
- package/dist/auth/browser-login.d.ts +30 -0
- package/dist/auth/browser-login.js +193 -0
- package/dist/auth/loopback-callback.d.ts +32 -0
- package/dist/auth/loopback-callback.js +175 -0
- package/dist/auth/open-browser.d.ts +15 -0
- package/dist/auth/open-browser.js +54 -0
- package/dist/auth/refresh.d.ts +27 -2
- package/dist/auth/refresh.js +52 -15
- package/dist/catalog/allowlist.d.ts +20 -4
- package/dist/catalog/allowlist.js +126 -25
- package/dist/catalog/command-tree.d.ts +35 -0
- package/dist/catalog/command-tree.js +119 -0
- package/dist/catalog/compatibility.d.ts +23 -0
- package/dist/catalog/compatibility.js +58 -0
- package/dist/catalog/generated/registry.json +6137 -0
- package/dist/catalog/generated/schemas.json +31036 -0
- package/dist/catalog/operations.d.ts +76 -3
- package/dist/catalog/operations.js +87 -213
- package/dist/catalog/validate-body.d.ts +22 -0
- package/dist/catalog/validate-body.js +98 -0
- package/dist/commands/request-body.d.ts +21 -0
- package/dist/commands/request-body.js +92 -0
- package/dist/commands/run.js +283 -162
- package/dist/config/profiles.js +3 -3
- package/dist/engine-backtest/errors.d.ts +18 -0
- package/dist/engine-backtest/errors.js +26 -0
- package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
- package/dist/engine-backtest/fetch-runtime.js +170 -0
- package/dist/engine-backtest/native-import.d.ts +1 -0
- package/dist/engine-backtest/native-import.js +10 -0
- package/dist/engine-backtest/parse-args.d.ts +6 -0
- package/dist/engine-backtest/parse-args.js +281 -0
- package/dist/engine-backtest/persist.d.ts +32 -0
- package/dist/engine-backtest/persist.js +94 -0
- package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
- package/dist/engine-backtest/replay-timeframe.js +48 -0
- package/dist/engine-backtest/resolve-packages.d.ts +27 -0
- package/dist/engine-backtest/resolve-packages.js +288 -0
- package/dist/engine-backtest/run-command.d.ts +38 -0
- package/dist/engine-backtest/run-command.js +540 -0
- package/dist/engine-backtest/types.d.ts +249 -0
- package/dist/engine-backtest/types.js +2 -0
- package/dist/envelope.d.ts +3 -0
- package/dist/envelope.js +43 -3
- package/dist/http/client.js +18 -15
- package/dist/index.d.ts +16 -5
- package/dist/index.js +47 -1
- package/dist/install/exec.d.ts +13 -0
- package/dist/install/exec.js +73 -0
- package/dist/install/package-root.d.ts +4 -0
- package/dist/install/package-root.js +59 -0
- package/dist/install/types.d.ts +69 -0
- package/dist/install/types.js +26 -0
- package/dist/install/wizard.d.ts +21 -0
- package/dist/install/wizard.js +332 -0
- package/dist/keychain/linux-secret-service.d.ts +11 -0
- package/dist/keychain/linux-secret-service.js +93 -0
- package/dist/keychain/store.d.ts +20 -1
- package/dist/keychain/store.js +94 -6
- package/dist/keychain/windows-credential.d.ts +13 -0
- package/dist/keychain/windows-credential.js +176 -0
- package/dist/safety/confirmation.d.ts +10 -3
- package/dist/safety/confirmation.js +27 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +3 -2
- package/docs/.nojekyll +0 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +156 -0
- package/docs/agents/triage-labels.md +18 -0
- package/docs/alphafox-cli-installation-guide.md +105 -0
- package/docs/e2e-staging.md +76 -6
- package/docs/favicon.svg +4 -0
- package/docs/index.html +748 -0
- package/docs/logo/alphafox-mark.svg +4 -0
- package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
- package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
- package/docs/release-supply-chain.md +107 -26
- package/package.json +10 -3
- package/skills/account/SKILL.md +8 -6
- package/skills/admin/SKILL.md +9 -4
- package/skills/alphafox-shared/SKILL.md +44 -14
- package/skills/auth/SKILL.md +21 -6
- package/skills/engine-backtest/SKILL.md +71 -0
- package/skills/exchange/SKILL.md +10 -3
- package/skills/market/SKILL.md +9 -4
- package/skills/notification/SKILL.md +8 -3
- package/skills/strategy/SKILL.md +23 -9
- package/skills/trading/SKILL.md +18 -5
- package/vendor/backtest-runner/NOTICE.md +1 -0
- package/vendor/backtest-runner/README.md +97 -0
- package/vendor/backtest-runner/index.d.ts +423 -0
- package/vendor/backtest-runner/index.mjs +59 -0
- package/vendor/backtest-runner/lib/abortable.mjs +23 -0
- package/vendor/backtest-runner/lib/cache.mjs +159 -0
- package/vendor/backtest-runner/lib/coverage.mjs +238 -0
- package/vendor/backtest-runner/lib/encode.mjs +28 -0
- package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
- package/vendor/backtest-runner/lib/proxy.mjs +78 -0
- package/vendor/backtest-runner/lib/scenario.mjs +66 -0
- package/vendor/backtest-runner/lib/series.mjs +370 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
- package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
- package/vendor/backtest-runner/package.json +13 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const DEFAULT_EXECUTION_MODEL = Object.freeze({
|
|
2
|
+
pricePath: "ohlc_path_4",
|
|
3
|
+
makerFeeRate: 0.0002,
|
|
4
|
+
takerFeeRate: 0.0005,
|
|
5
|
+
slippageRate: 0.0001,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const PRICE_PATHS = new Set(["ohlc_path_4", "close_only"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Assemble an EngineBacktestScenario from a prepared tape.
|
|
12
|
+
* Does not execute wasm.
|
|
13
|
+
*
|
|
14
|
+
* @param {import("../index.d.ts").AssembleScenarioInput} input
|
|
15
|
+
* @returns {import("../index.d.ts").EngineBacktestScenario}
|
|
16
|
+
*/
|
|
17
|
+
export function assembleScenario(input) {
|
|
18
|
+
const tape = input?.tape ?? input?.preparedTape?.tape ?? input?.prepared?.tape;
|
|
19
|
+
if (!tape) {
|
|
20
|
+
throw new Error("assembleScenario requires a prepared tape");
|
|
21
|
+
}
|
|
22
|
+
if (typeof input.runId !== "string" || input.runId.trim() === "") {
|
|
23
|
+
throw new Error("assembleScenario requires runId");
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
typeof input.definitionId !== "string" ||
|
|
27
|
+
input.definitionId.trim() === ""
|
|
28
|
+
) {
|
|
29
|
+
throw new Error("assembleScenario requires definitionId");
|
|
30
|
+
}
|
|
31
|
+
if (!Number.isFinite(input.initialEquity)) {
|
|
32
|
+
throw new Error("assembleScenario requires initialEquity");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
version: 1,
|
|
37
|
+
runId: input.runId,
|
|
38
|
+
trader: {
|
|
39
|
+
...(input.traderId ? { id: input.traderId } : {}),
|
|
40
|
+
...(input.traderName ? { name: input.traderName } : {}),
|
|
41
|
+
strategyDefinitionId: input.definitionId,
|
|
42
|
+
configSchemaVersion: input.configSchemaVersion,
|
|
43
|
+
subscriptionTier: input.subscriptionTier,
|
|
44
|
+
config: structuredClone(input.config),
|
|
45
|
+
},
|
|
46
|
+
exchange: {
|
|
47
|
+
positionSideDual: true,
|
|
48
|
+
initialEquity: input.initialEquity,
|
|
49
|
+
},
|
|
50
|
+
executionModel: resolveExecutionModel(input.executionModel),
|
|
51
|
+
tape,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveExecutionModel(override) {
|
|
56
|
+
const merged = {
|
|
57
|
+
pricePath: override?.pricePath ?? DEFAULT_EXECUTION_MODEL.pricePath,
|
|
58
|
+
makerFeeRate: override?.makerFeeRate ?? DEFAULT_EXECUTION_MODEL.makerFeeRate,
|
|
59
|
+
takerFeeRate: override?.takerFeeRate ?? DEFAULT_EXECUTION_MODEL.takerFeeRate,
|
|
60
|
+
slippageRate: override?.slippageRate ?? DEFAULT_EXECUTION_MODEL.slippageRate,
|
|
61
|
+
};
|
|
62
|
+
if (!PRICE_PATHS.has(merged.pricePath)) {
|
|
63
|
+
throw new Error(`Unsupported executionModel.pricePath: ${merged.pricePath}`);
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { abortable } from "./abortable.mjs";
|
|
2
|
+
import {
|
|
3
|
+
missingCachedOhlcvRanges,
|
|
4
|
+
mergeCachedOhlcvRanges,
|
|
5
|
+
ohlcvSeriesCacheKey,
|
|
6
|
+
} from "./cache.mjs";
|
|
7
|
+
import {
|
|
8
|
+
evaluateOhlcvCoverage,
|
|
9
|
+
TapeDataUnavailableError,
|
|
10
|
+
} from "./coverage.mjs";
|
|
11
|
+
import {
|
|
12
|
+
ENGINE_BACKTEST_WARMUP_CANDLES,
|
|
13
|
+
TIMEFRAME_MS,
|
|
14
|
+
} from "./timeframes.mjs";
|
|
15
|
+
|
|
16
|
+
const WARMUP_CANDLES = ENGINE_BACKTEST_WARMUP_CANDLES;
|
|
17
|
+
const BITGET_OHLCV_MAX_REQUEST_SPAN_MS = 89 * 86_400_000;
|
|
18
|
+
|
|
19
|
+
export { TIMEFRAME_MS };
|
|
20
|
+
|
|
21
|
+
export function isClosedCandle(timestampMs, timeframe, toMs) {
|
|
22
|
+
const stepMs = TIMEFRAME_MS[timeframe];
|
|
23
|
+
if (!stepMs) {
|
|
24
|
+
throw new Error(`不支持的 timeframe:${timeframe}`);
|
|
25
|
+
}
|
|
26
|
+
return timestampMs + stepMs <= toMs;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ohlcvSeriesStartMs(fromMs, timeframe, market) {
|
|
30
|
+
const stepMs = TIMEFRAME_MS[timeframe];
|
|
31
|
+
if (!stepMs) {
|
|
32
|
+
throw new Error(`不支持的 timeframe:${timeframe}`);
|
|
33
|
+
}
|
|
34
|
+
const warmupStartMs = fromMs - WARMUP_CANDLES * stepMs;
|
|
35
|
+
const marketCreatedMs = market?.created;
|
|
36
|
+
return typeof marketCreatedMs === "number" &&
|
|
37
|
+
Number.isFinite(marketCreatedMs) &&
|
|
38
|
+
marketCreatedMs > 0
|
|
39
|
+
? Math.max(warmupStartMs, marketCreatedMs)
|
|
40
|
+
: warmupStartMs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadSeriesWithCache(
|
|
44
|
+
exchange,
|
|
45
|
+
exchangeDefinition,
|
|
46
|
+
runtimeConfig,
|
|
47
|
+
symbol,
|
|
48
|
+
timeframe,
|
|
49
|
+
market,
|
|
50
|
+
fromMs,
|
|
51
|
+
toMs,
|
|
52
|
+
minWarmupCandles,
|
|
53
|
+
requireFullReplayCoverage,
|
|
54
|
+
dataQualityMode,
|
|
55
|
+
onFraction,
|
|
56
|
+
cacheUntilMs,
|
|
57
|
+
cache,
|
|
58
|
+
signal
|
|
59
|
+
) {
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
const stepMs = TIMEFRAME_MS[timeframe];
|
|
62
|
+
if (!stepMs) {
|
|
63
|
+
throw new Error(`不支持的 timeframe:${timeframe}`);
|
|
64
|
+
}
|
|
65
|
+
const sinceMs = ohlcvSeriesStartMs(fromMs, timeframe, market);
|
|
66
|
+
const closedEndMs = Math.min(Math.max(cacheUntilMs, sinceMs), toMs);
|
|
67
|
+
const cacheable = closedEndMs > sinceMs;
|
|
68
|
+
const cacheKey = ohlcvSeriesCacheKey(
|
|
69
|
+
exchangeDefinition.ccxtId,
|
|
70
|
+
symbol,
|
|
71
|
+
timeframe
|
|
72
|
+
);
|
|
73
|
+
const totalSpan = Math.max(1, toMs - sinceMs);
|
|
74
|
+
const closedShare = (closedEndMs - sinceMs) / totalSpan;
|
|
75
|
+
const closedSpan = Math.max(1, closedEndMs - sinceMs);
|
|
76
|
+
|
|
77
|
+
const evaluate = (rows) =>
|
|
78
|
+
evaluateOhlcvCoverage({
|
|
79
|
+
symbol,
|
|
80
|
+
timeframe,
|
|
81
|
+
rows,
|
|
82
|
+
stepMs,
|
|
83
|
+
fromMs,
|
|
84
|
+
toMs,
|
|
85
|
+
minWarmupCandles,
|
|
86
|
+
requireFullReplayCoverage,
|
|
87
|
+
mode: dataQualityMode,
|
|
88
|
+
});
|
|
89
|
+
const fetchClosedSegment = (range, reportProgress) =>
|
|
90
|
+
fetchClosedOhlcvRange({
|
|
91
|
+
exchange,
|
|
92
|
+
exchangeDefinition,
|
|
93
|
+
runtimeConfig,
|
|
94
|
+
symbol,
|
|
95
|
+
timeframe,
|
|
96
|
+
sinceMs: range.sinceMs,
|
|
97
|
+
fromMs: Math.min(range.untilMs, Math.max(range.sinceMs, fromMs)),
|
|
98
|
+
toMs: range.untilMs,
|
|
99
|
+
allowEmpty: true,
|
|
100
|
+
onFraction: reportProgress
|
|
101
|
+
? (fraction) =>
|
|
102
|
+
onFraction(
|
|
103
|
+
((range.sinceMs -
|
|
104
|
+
sinceMs +
|
|
105
|
+
fraction * (range.untilMs - range.sinceMs)) /
|
|
106
|
+
closedSpan) *
|
|
107
|
+
closedShare
|
|
108
|
+
)
|
|
109
|
+
: undefined,
|
|
110
|
+
signal,
|
|
111
|
+
});
|
|
112
|
+
const fetchLiveTail = () =>
|
|
113
|
+
closedEndMs < toMs
|
|
114
|
+
? fetchClosedOhlcvRange({
|
|
115
|
+
exchange,
|
|
116
|
+
exchangeDefinition,
|
|
117
|
+
runtimeConfig,
|
|
118
|
+
symbol,
|
|
119
|
+
timeframe,
|
|
120
|
+
sinceMs: closedEndMs,
|
|
121
|
+
fromMs: Math.max(fromMs, closedEndMs),
|
|
122
|
+
toMs,
|
|
123
|
+
allowEmpty: true,
|
|
124
|
+
onFraction: (fraction) =>
|
|
125
|
+
onFraction(closedShare + fraction * (1 - closedShare)),
|
|
126
|
+
signal,
|
|
127
|
+
})
|
|
128
|
+
: Promise.resolve([]);
|
|
129
|
+
|
|
130
|
+
const cachedRange =
|
|
131
|
+
cacheable && cache && !cache.disabled
|
|
132
|
+
? await abortable(cache.read(cacheKey), signal)
|
|
133
|
+
: null;
|
|
134
|
+
signal?.throwIfAborted();
|
|
135
|
+
const cachedRangeContributed =
|
|
136
|
+
cachedRange !== null &&
|
|
137
|
+
cachedRange.untilMs >= sinceMs &&
|
|
138
|
+
cachedRange.sinceMs <= closedEndMs;
|
|
139
|
+
const missingRanges = cacheable
|
|
140
|
+
? missingCachedOhlcvRanges(cachedRange, sinceMs, closedEndMs)
|
|
141
|
+
: [];
|
|
142
|
+
let mergedRange = cachedRange;
|
|
143
|
+
let fetchedClosedRows = false;
|
|
144
|
+
for (const missingRange of missingRanges) {
|
|
145
|
+
onFraction(((missingRange.sinceMs - sinceMs) / closedSpan) * closedShare);
|
|
146
|
+
const fetchedRows = await fetchClosedSegment(missingRange, true);
|
|
147
|
+
mergedRange = mergeCachedOhlcvRanges(mergedRange, {
|
|
148
|
+
...missingRange,
|
|
149
|
+
rows: fetchedRows,
|
|
150
|
+
});
|
|
151
|
+
fetchedClosedRows = true;
|
|
152
|
+
}
|
|
153
|
+
if (cacheable && missingRanges.length === 0) onFraction(closedShare);
|
|
154
|
+
|
|
155
|
+
let closedRows = rowsForClosedRange(
|
|
156
|
+
mergedRange,
|
|
157
|
+
sinceMs,
|
|
158
|
+
closedEndMs,
|
|
159
|
+
timeframe
|
|
160
|
+
);
|
|
161
|
+
const liveRows = await fetchLiveTail();
|
|
162
|
+
signal?.throwIfAborted();
|
|
163
|
+
|
|
164
|
+
let rows = concatMonotonicRows(closedRows ?? [], liveRows);
|
|
165
|
+
let decision = evaluate(rows);
|
|
166
|
+
if (!decision.accepted && cachedRangeContributed) {
|
|
167
|
+
const freshRange = { sinceMs, untilMs: closedEndMs };
|
|
168
|
+
closedRows = await fetchClosedSegment(freshRange, false);
|
|
169
|
+
mergedRange = mergeCachedOhlcvRanges(mergedRange, {
|
|
170
|
+
...freshRange,
|
|
171
|
+
rows: closedRows,
|
|
172
|
+
});
|
|
173
|
+
fetchedClosedRows = true;
|
|
174
|
+
rows = concatMonotonicRows(closedRows, liveRows);
|
|
175
|
+
decision = evaluate(rows);
|
|
176
|
+
}
|
|
177
|
+
if (!decision.accepted) {
|
|
178
|
+
throw new TapeDataUnavailableError(decision.blockingIssues);
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
cacheable &&
|
|
182
|
+
fetchedClosedRows &&
|
|
183
|
+
mergedRange !== null &&
|
|
184
|
+
mergedRange.rows.length > 0 &&
|
|
185
|
+
cache &&
|
|
186
|
+
!cache.disabled
|
|
187
|
+
) {
|
|
188
|
+
await abortable(cache.write(cacheKey, mergedRange), signal);
|
|
189
|
+
}
|
|
190
|
+
signal?.throwIfAborted();
|
|
191
|
+
onFraction(1);
|
|
192
|
+
return {
|
|
193
|
+
rows,
|
|
194
|
+
softIssues: decision.acceptedSoftIssues,
|
|
195
|
+
coverageRatio: decision.report.coverageRatio,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rowsForClosedRange(range, sinceMs, untilMs, timeframe) {
|
|
200
|
+
return (range?.rows ?? []).filter(
|
|
201
|
+
(row) => row[0] >= sinceMs && isClosedCandle(row[0], timeframe, untilMs)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function concatMonotonicRows(head, tail) {
|
|
206
|
+
if (head.length === 0) return [...tail];
|
|
207
|
+
const lastHeadTimestamp = head[head.length - 1][0];
|
|
208
|
+
return [...head, ...tail.filter((row) => row[0] > lastHeadTimestamp)];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function fetchClosedOhlcvRange(request) {
|
|
212
|
+
const stepMs = TIMEFRAME_MS[request.timeframe];
|
|
213
|
+
if (!stepMs) {
|
|
214
|
+
throw new Error(`不支持的 timeframe:${request.timeframe}`);
|
|
215
|
+
}
|
|
216
|
+
const pageLimit = ohlcvPageLimitForTimeframe(
|
|
217
|
+
request.exchangeDefinition,
|
|
218
|
+
request.runtimeConfig,
|
|
219
|
+
request.timeframe
|
|
220
|
+
);
|
|
221
|
+
const rows = [];
|
|
222
|
+
const totalSpan = Math.max(1, request.toMs - request.sinceMs);
|
|
223
|
+
let cursor = request.sinceMs;
|
|
224
|
+
let emptyPrefixLowerMs = null;
|
|
225
|
+
let nonEmptyPrefixUpperMs = null;
|
|
226
|
+
let prefixResolved = false;
|
|
227
|
+
|
|
228
|
+
while (cursor < request.toMs) {
|
|
229
|
+
request.signal?.throwIfAborted();
|
|
230
|
+
const fetchSinceMs =
|
|
231
|
+
request.exchangeDefinition.ccxtId === "bitget"
|
|
232
|
+
? Math.max(0, cursor - 1)
|
|
233
|
+
: cursor;
|
|
234
|
+
const requestParams = {
|
|
235
|
+
...request.runtimeConfig.requestParams,
|
|
236
|
+
};
|
|
237
|
+
if (request.exchangeDefinition.ccxtId === "hyperliquid") {
|
|
238
|
+
requestParams.until = Math.min(
|
|
239
|
+
request.toMs,
|
|
240
|
+
fetchSinceMs + (pageLimit - 1) * stepMs
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const page = await abortable(
|
|
244
|
+
request.exchange.fetchOHLCV(
|
|
245
|
+
request.symbol,
|
|
246
|
+
request.timeframe,
|
|
247
|
+
fetchSinceMs,
|
|
248
|
+
pageLimit,
|
|
249
|
+
requestParams
|
|
250
|
+
),
|
|
251
|
+
request.signal
|
|
252
|
+
);
|
|
253
|
+
request.signal?.throwIfAborted();
|
|
254
|
+
|
|
255
|
+
if (!prefixResolved && rows.length === 0 && emptyPrefixLowerMs !== null) {
|
|
256
|
+
if (page.length === 0) {
|
|
257
|
+
emptyPrefixLowerMs = cursor;
|
|
258
|
+
if (cursor >= request.fromMs) break;
|
|
259
|
+
} else {
|
|
260
|
+
nonEmptyPrefixUpperMs = cursor;
|
|
261
|
+
}
|
|
262
|
+
const upperMs = nonEmptyPrefixUpperMs ?? request.fromMs;
|
|
263
|
+
if (
|
|
264
|
+
nonEmptyPrefixUpperMs !== null &&
|
|
265
|
+
upperMs - emptyPrefixLowerMs <= stepMs
|
|
266
|
+
) {
|
|
267
|
+
cursor = upperMs;
|
|
268
|
+
prefixResolved = true;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
cursor = midpointCursorMs(emptyPrefixLowerMs, upperMs, stepMs);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (page.length === 0) {
|
|
276
|
+
if (!prefixResolved && rows.length === 0 && cursor < request.fromMs) {
|
|
277
|
+
emptyPrefixLowerMs = cursor;
|
|
278
|
+
cursor = midpointCursorMs(cursor, request.fromMs, stepMs);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
prefixResolved = true;
|
|
284
|
+
|
|
285
|
+
for (const raw of page) {
|
|
286
|
+
if (raw.length < 6) {
|
|
287
|
+
throw new TapeDataUnavailableError([
|
|
288
|
+
{
|
|
289
|
+
code: "invalid_ohlcv",
|
|
290
|
+
symbol: request.symbol,
|
|
291
|
+
timeframe: request.timeframe,
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
const timestamp = Number(raw[0]);
|
|
296
|
+
if (!Number.isFinite(timestamp)) {
|
|
297
|
+
throw new TapeDataUnavailableError([
|
|
298
|
+
{
|
|
299
|
+
code: "invalid_ohlcv",
|
|
300
|
+
symbol: request.symbol,
|
|
301
|
+
timeframe: request.timeframe,
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
if (!isClosedCandle(timestamp, request.timeframe, request.toMs)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (rows.length > 0 && timestamp <= rows[rows.length - 1][0]) {
|
|
309
|
+
throw new TapeDataUnavailableError([
|
|
310
|
+
{
|
|
311
|
+
code: "non_monotonic",
|
|
312
|
+
symbol: request.symbol,
|
|
313
|
+
timeframe: request.timeframe,
|
|
314
|
+
expected: rows[rows.length - 1][0] + stepMs,
|
|
315
|
+
actual: timestamp,
|
|
316
|
+
timestamp,
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
rows.push([
|
|
321
|
+
timestamp,
|
|
322
|
+
Number(raw[1]),
|
|
323
|
+
Number(raw[2]),
|
|
324
|
+
Number(raw[3]),
|
|
325
|
+
Number(raw[4]),
|
|
326
|
+
Number(raw[5]),
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
const lastTimestamp = Number(page[page.length - 1][0]);
|
|
330
|
+
const nextCursor = lastTimestamp + stepMs;
|
|
331
|
+
if (nextCursor <= cursor) {
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
cursor = nextCursor;
|
|
335
|
+
request.onFraction?.(Math.min(1, (cursor - request.sinceMs) / totalSpan));
|
|
336
|
+
}
|
|
337
|
+
if (rows.length === 0 && !request.allowEmpty) {
|
|
338
|
+
throw new TapeDataUnavailableError([
|
|
339
|
+
{
|
|
340
|
+
code: "ohlcv_missing",
|
|
341
|
+
symbol: request.symbol,
|
|
342
|
+
timeframe: request.timeframe,
|
|
343
|
+
},
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
return rows;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function ohlcvPageLimitForTimeframe(exchange, runtimeConfig, timeframe) {
|
|
350
|
+
if (exchange.ccxtId !== "bitget") {
|
|
351
|
+
return runtimeConfig.ohlcvPageLimit;
|
|
352
|
+
}
|
|
353
|
+
const stepMs = TIMEFRAME_MS[timeframe];
|
|
354
|
+
return Math.max(
|
|
355
|
+
1,
|
|
356
|
+
Math.min(
|
|
357
|
+
runtimeConfig.ohlcvPageLimit,
|
|
358
|
+
Math.floor(BITGET_OHLCV_MAX_REQUEST_SPAN_MS / stepMs)
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function midpointCursorMs(lowerMs, upperMs, stepMs) {
|
|
364
|
+
if (upperMs - lowerMs <= stepMs) {
|
|
365
|
+
return upperMs;
|
|
366
|
+
}
|
|
367
|
+
const midpointMs = lowerMs + Math.floor((upperMs - lowerMs) / 2);
|
|
368
|
+
const alignedMs = Math.floor(midpointMs / stepMs) * stepMs;
|
|
369
|
+
return Math.min(upperMs, Math.max(lowerMs + 1, alignedMs));
|
|
370
|
+
}
|