@alphafox/cli 0.3.7 → 0.3.9
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.d.ts +3 -0
- package/dist/auth/refresh.js +98 -12
- package/dist/cache/clean.d.ts +1 -0
- package/dist/cache/clean.js +12 -0
- package/dist/cache/inspect.d.ts +7 -0
- package/dist/cache/inspect.js +31 -0
- package/dist/cache/paths.d.ts +5 -0
- package/dist/cache/paths.js +41 -0
- package/dist/cache/run-command.d.ts +7 -0
- package/dist/cache/run-command.js +85 -0
- package/dist/commands/run.js +44 -11
- package/dist/engine-backtest/run-command.js +2 -0
- package/dist/engine-backtest/sweep-command.js +2 -0
- package/dist/engine-backtest/types.d.ts +1 -0
- package/dist/skills-manifest.json +51 -39
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +14 -3
- package/skills/alphafox-shared/SKILL.md +3 -2
- package/skills/auth/SKILL.md +8 -5
- package/skills/cache/SKILL.md +38 -0
- package/skills/engine-backtest/SKILL.md +2 -1
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +1 -1
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +1 -1
- package/skills/trading/SKILL.md +1 -1
- package/vendor/backtest-runner/index.d.ts +15 -0
- package/vendor/backtest-runner/index.mjs +4 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +127 -53
package/dist/auth/refresh.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { ProfileConfig } from "../config/profiles";
|
|
|
8
8
|
import { type StoredTokens } from "../keychain/store";
|
|
9
9
|
/** Refresh when access token expires within this window. */
|
|
10
10
|
export declare const ACCESS_TOKEN_REFRESH_SKEW_MS = 60000;
|
|
11
|
+
/** Drop a stale inter-process refresh lock after this long. */
|
|
12
|
+
export declare const REFRESH_LOCK_STALE_MS = 30000;
|
|
11
13
|
export type RefreshOutcome = {
|
|
12
14
|
readonly status: "refreshed";
|
|
13
15
|
readonly tokens: StoredTokens;
|
|
@@ -40,5 +42,6 @@ export declare function refreshStoredTokensOrNull(profile: ProfileConfig, env?:
|
|
|
40
42
|
readonly now?: number;
|
|
41
43
|
readonly force?: boolean;
|
|
42
44
|
}): Promise<StoredTokens | null>;
|
|
45
|
+
export declare function refreshLockFilePath(profile: string, env?: NodeJS.ProcessEnv): string;
|
|
43
46
|
/** Test helper: clear in-flight map between cases. */
|
|
44
47
|
export declare function clearRefreshInflightForTests(): void;
|
package/dist/auth/refresh.js
CHANGED
|
@@ -6,15 +6,21 @@
|
|
|
6
6
|
* Outcomes are explicit: callers must not treat a failed refresh as a healthy session.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.ACCESS_TOKEN_REFRESH_SKEW_MS = void 0;
|
|
9
|
+
exports.REFRESH_LOCK_STALE_MS = exports.ACCESS_TOKEN_REFRESH_SKEW_MS = void 0;
|
|
10
10
|
exports.accessTokenNeedsRefresh = accessTokenNeedsRefresh;
|
|
11
11
|
exports.refreshStoredTokens = refreshStoredTokens;
|
|
12
12
|
exports.refreshStoredTokensOrNull = refreshStoredTokensOrNull;
|
|
13
|
+
exports.refreshLockFilePath = refreshLockFilePath;
|
|
13
14
|
exports.clearRefreshInflightForTests = clearRefreshInflightForTests;
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
const node_os_1 = require("node:os");
|
|
14
18
|
const version_1 = require("../version");
|
|
15
19
|
const store_1 = require("../keychain/store");
|
|
16
20
|
/** Refresh when access token expires within this window. */
|
|
17
21
|
exports.ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
22
|
+
/** Drop a stale inter-process refresh lock after this long. */
|
|
23
|
+
exports.REFRESH_LOCK_STALE_MS = 30_000;
|
|
18
24
|
/** In-flight refresh promises so concurrent API calls share one rotation. */
|
|
19
25
|
const inflightByProfile = new Map();
|
|
20
26
|
function accessTokenNeedsRefresh(tokens, now = Date.now()) {
|
|
@@ -36,20 +42,38 @@ async function refreshStoredTokens(profile, env = process.env, fetchImpl = fetch
|
|
|
36
42
|
tokens: null,
|
|
37
43
|
};
|
|
38
44
|
}
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const now = options.now ?? Date.now();
|
|
46
|
+
if (!options.force && !accessTokenNeedsRefresh(existing, now)) {
|
|
41
47
|
return { status: "unchanged", tokens: existing };
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
return withRefreshLock(profile.name, env, async () => {
|
|
50
|
+
const latest = (0, store_1.loadTokens)(profile.name, env) ?? existing;
|
|
51
|
+
if (!latest?.refreshToken?.trim()) {
|
|
52
|
+
return {
|
|
53
|
+
status: "no_session",
|
|
54
|
+
reason: "no_refresh_token",
|
|
55
|
+
tokens: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const someoneElseRefreshed = latest.refreshToken !== existing.refreshToken ||
|
|
59
|
+
latest.expiresAt > existing.expiresAt;
|
|
60
|
+
if (someoneElseRefreshed && !accessTokenNeedsRefresh(latest, now)) {
|
|
61
|
+
return { status: "unchanged", tokens: latest };
|
|
62
|
+
}
|
|
63
|
+
if (!options.force && !accessTokenNeedsRefresh(latest, now)) {
|
|
64
|
+
return { status: "unchanged", tokens: latest };
|
|
65
|
+
}
|
|
66
|
+
const key = profile.name;
|
|
67
|
+
const pending = inflightByProfile.get(key);
|
|
68
|
+
if (pending) {
|
|
69
|
+
return pending;
|
|
70
|
+
}
|
|
71
|
+
const work = performRefresh(profile, latest, env, fetchImpl).finally(() => {
|
|
72
|
+
inflightByProfile.delete(key);
|
|
73
|
+
});
|
|
74
|
+
inflightByProfile.set(key, work);
|
|
75
|
+
return work;
|
|
50
76
|
});
|
|
51
|
-
inflightByProfile.set(key, work);
|
|
52
|
-
return work;
|
|
53
77
|
}
|
|
54
78
|
/**
|
|
55
79
|
* Convenience for callers that only need tokens on successful refresh/unchanged.
|
|
@@ -62,6 +86,68 @@ async function refreshStoredTokensOrNull(profile, env = process.env, fetchImpl =
|
|
|
62
86
|
}
|
|
63
87
|
return null;
|
|
64
88
|
}
|
|
89
|
+
function refreshLockFilePath(profile, env = process.env) {
|
|
90
|
+
const base = env.ALPHAFOX_KEYCHAIN_DIR?.trim() ||
|
|
91
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "alphafox", "keychain");
|
|
92
|
+
return (0, node_path_1.join)(base, `${profile}.refresh.lock`);
|
|
93
|
+
}
|
|
94
|
+
async function withRefreshLock(profile, env, work) {
|
|
95
|
+
const path = refreshLockFilePath(profile, env);
|
|
96
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
97
|
+
const started = Date.now();
|
|
98
|
+
while (true) {
|
|
99
|
+
try {
|
|
100
|
+
const fd = (0, node_fs_1.openSync)(path, node_fs_1.constants.O_CREAT | node_fs_1.constants.O_EXCL | node_fs_1.constants.O_WRONLY);
|
|
101
|
+
try {
|
|
102
|
+
(0, node_fs_1.writeFileSync)(fd, `${process.pid}\n${Date.now()}\n`);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
(0, node_fs_1.closeSync)(fd);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await work();
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
try {
|
|
112
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// another process stole a stale lock
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const code = err.code;
|
|
121
|
+
if (code !== "EEXIST") {
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
if (Date.now() - (0, node_fs_1.statSync)(path).mtimeMs > exports.REFRESH_LOCK_STALE_MS) {
|
|
126
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// lock disappeared; retry acquire
|
|
132
|
+
}
|
|
133
|
+
if (Date.now() - started > exports.REFRESH_LOCK_STALE_MS + 5_000) {
|
|
134
|
+
try {
|
|
135
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// raced
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
await sleep(50);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function sleep(ms) {
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
setTimeout(resolve, ms);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
65
151
|
async function performRefresh(profile, existing, env, fetchImpl) {
|
|
66
152
|
const origin = profile.apiBaseUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
|
|
67
153
|
const url = `${origin}/api/auth/oauth/token`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function removeCacheRoot(directory: string, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeCacheRoot = removeCacheRoot;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const paths_1 = require("./paths");
|
|
6
|
+
function removeCacheRoot(directory, env = process.env) {
|
|
7
|
+
(0, paths_1.assertSafeCacheRoot)(directory, env);
|
|
8
|
+
if (!(0, node_fs_1.existsSync)(directory)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
(0, node_fs_1.rmSync)(directory, { recursive: true, force: true });
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectDirectory = inspectDirectory;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
function inspectDirectory(path) {
|
|
7
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
8
|
+
return { path, exists: false, bytes: 0, files: 0 };
|
|
9
|
+
}
|
|
10
|
+
const root = (0, node_fs_1.statSync)(path);
|
|
11
|
+
if (root.isFile()) {
|
|
12
|
+
return { path, exists: true, bytes: root.size, files: 1 };
|
|
13
|
+
}
|
|
14
|
+
let bytes = 0;
|
|
15
|
+
let files = 0;
|
|
16
|
+
const walk = (dir) => {
|
|
17
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
18
|
+
const next = (0, node_path_1.join)(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
walk(next);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (entry.isFile()) {
|
|
24
|
+
files += 1;
|
|
25
|
+
bytes += (0, node_fs_1.statSync)(next).size;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
walk(path);
|
|
30
|
+
return { path, exists: true, bytes, files };
|
|
31
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Remind Agents to offer cleanup at or above this tape-cache size. */
|
|
2
|
+
export declare const TAPE_CACHE_REMIND_BYTES: number;
|
|
3
|
+
export declare function resolveTapeCacheDir(env?: NodeJS.ProcessEnv): string;
|
|
4
|
+
export declare function resolveRuntimeCacheRoot(env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
export declare function assertSafeCacheRoot(directory: string, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TAPE_CACHE_REMIND_BYTES = void 0;
|
|
4
|
+
exports.resolveTapeCacheDir = resolveTapeCacheDir;
|
|
5
|
+
exports.resolveRuntimeCacheRoot = resolveRuntimeCacheRoot;
|
|
6
|
+
exports.assertSafeCacheRoot = assertSafeCacheRoot;
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
/** Remind Agents to offer cleanup at or above this tape-cache size. */
|
|
10
|
+
exports.TAPE_CACHE_REMIND_BYTES = 512 * 1024 * 1024;
|
|
11
|
+
function resolveTapeCacheDir(env = process.env) {
|
|
12
|
+
const override = env.ALPHAFOX_TAPE_CACHE_DIR?.trim();
|
|
13
|
+
if (override) {
|
|
14
|
+
return (0, node_path_1.resolve)(override);
|
|
15
|
+
}
|
|
16
|
+
return (0, node_path_1.resolve)((0, node_path_1.join)((0, node_os_1.homedir)(), ".alphafox", "cache", "engine-backtest"));
|
|
17
|
+
}
|
|
18
|
+
function resolveRuntimeCacheRoot(env = process.env) {
|
|
19
|
+
const override = env.ALPHAFOX_BACKTEST_RUNTIME_CACHE_DIR?.trim();
|
|
20
|
+
if (override) {
|
|
21
|
+
return (0, node_path_1.resolve)(override);
|
|
22
|
+
}
|
|
23
|
+
const xdg = env.XDG_CACHE_HOME?.trim();
|
|
24
|
+
return (0, node_path_1.resolve)((0, node_path_1.join)(xdg || (0, node_path_1.join)((0, node_os_1.homedir)(), ".cache"), "alphafox", "engine-backtest"));
|
|
25
|
+
}
|
|
26
|
+
function assertSafeCacheRoot(directory, env = process.env) {
|
|
27
|
+
const resolved = (0, node_path_1.resolve)(directory);
|
|
28
|
+
const tape = resolveTapeCacheDir(env);
|
|
29
|
+
const runtime = resolveRuntimeCacheRoot(env);
|
|
30
|
+
if (resolved === tape || resolved === runtime) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if ((0, node_path_1.basename)(resolved) === "engine-backtest") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
throw Object.assign(new Error(`Refusing to touch cache directory ${resolved}`), {
|
|
37
|
+
type: "usage",
|
|
38
|
+
subtype: "cache_root_unsafe",
|
|
39
|
+
status: 400,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CacheCliFlags {
|
|
2
|
+
readonly format: "json" | "jsonl" | "text";
|
|
3
|
+
readonly yes: boolean;
|
|
4
|
+
readonly dryRun: boolean;
|
|
5
|
+
readonly jq?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function cmdCache(args: string[], flags: CacheCliFlags, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdCache = cmdCache;
|
|
4
|
+
const envelope_1 = require("../envelope");
|
|
5
|
+
const clean_1 = require("./clean");
|
|
6
|
+
const inspect_1 = require("./inspect");
|
|
7
|
+
const paths_1 = require("./paths");
|
|
8
|
+
async function cmdCache(args, flags, env = process.env) {
|
|
9
|
+
const sub = args[0];
|
|
10
|
+
if (sub === "status" || !sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
11
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
12
|
+
(0, envelope_1.writeSuccess)({
|
|
13
|
+
name: "cache",
|
|
14
|
+
usage: [
|
|
15
|
+
"alphafox cache status",
|
|
16
|
+
"alphafox cache clean [--tape|--runtime|--all] [--yes|--dry-run]",
|
|
17
|
+
],
|
|
18
|
+
}, { format: flags.format, jq: flags.jq });
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const tape = (0, inspect_1.inspectDirectory)((0, paths_1.resolveTapeCacheDir)(env));
|
|
22
|
+
const runtime = (0, inspect_1.inspectDirectory)((0, paths_1.resolveRuntimeCacheRoot)(env));
|
|
23
|
+
(0, envelope_1.writeSuccess)({
|
|
24
|
+
tape: { ...tape, large: tape.bytes >= paths_1.TAPE_CACHE_REMIND_BYTES },
|
|
25
|
+
runtime,
|
|
26
|
+
remindAfterBytes: paths_1.TAPE_CACHE_REMIND_BYTES,
|
|
27
|
+
totalBytes: tape.bytes + runtime.bytes,
|
|
28
|
+
}, { format: flags.format, jq: flags.jq });
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (sub === "clean") {
|
|
32
|
+
const rest = args.slice(1);
|
|
33
|
+
const all = rest.includes("--all");
|
|
34
|
+
const runtimeOnly = rest.includes("--runtime");
|
|
35
|
+
const tapeOnly = rest.includes("--tape") || (!all && !runtimeOnly);
|
|
36
|
+
const clearTape = all || tapeOnly;
|
|
37
|
+
const clearRuntime = all || runtimeOnly;
|
|
38
|
+
const tapePath = (0, paths_1.resolveTapeCacheDir)(env);
|
|
39
|
+
const runtimePath = (0, paths_1.resolveRuntimeCacheRoot)(env);
|
|
40
|
+
const tapeBefore = (0, inspect_1.inspectDirectory)(tapePath);
|
|
41
|
+
const runtimeBefore = (0, inspect_1.inspectDirectory)(runtimePath);
|
|
42
|
+
if (flags.dryRun) {
|
|
43
|
+
(0, envelope_1.writeSuccess)({
|
|
44
|
+
dryRun: true,
|
|
45
|
+
cleared: [
|
|
46
|
+
...(clearTape ? ["tape"] : []),
|
|
47
|
+
...(clearRuntime ? ["runtime"] : []),
|
|
48
|
+
],
|
|
49
|
+
tape: tapeBefore,
|
|
50
|
+
runtime: runtimeBefore,
|
|
51
|
+
}, { format: flags.format, jq: flags.jq });
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
if (!flags.yes) {
|
|
55
|
+
(0, envelope_1.writeError)({
|
|
56
|
+
type: "confirmation",
|
|
57
|
+
subtype: "yes_required",
|
|
58
|
+
message: "Cleaning local backtest cache requires --yes (or --dry-run).",
|
|
59
|
+
status: 400,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (clearTape) {
|
|
63
|
+
(0, clean_1.removeCacheRoot)(tapePath, env);
|
|
64
|
+
}
|
|
65
|
+
if (clearRuntime) {
|
|
66
|
+
(0, clean_1.removeCacheRoot)(runtimePath, env);
|
|
67
|
+
}
|
|
68
|
+
(0, envelope_1.writeSuccess)({
|
|
69
|
+
dryRun: false,
|
|
70
|
+
cleared: [
|
|
71
|
+
...(clearTape ? ["tape"] : []),
|
|
72
|
+
...(clearRuntime ? ["runtime"] : []),
|
|
73
|
+
],
|
|
74
|
+
tape: (0, inspect_1.inspectDirectory)(tapePath),
|
|
75
|
+
runtime: (0, inspect_1.inspectDirectory)(runtimePath),
|
|
76
|
+
bytesFreed: (clearTape ? tapeBefore.bytes : 0) +
|
|
77
|
+
(clearRuntime ? runtimeBefore.bytes : 0),
|
|
78
|
+
}, { format: flags.format, jq: flags.jq });
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
(0, envelope_1.writeError)({
|
|
82
|
+
type: "usage",
|
|
83
|
+
message: "Usage: alphafox cache status|clean",
|
|
84
|
+
});
|
|
85
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -9,15 +9,17 @@ const allowlist_1 = require("../catalog/allowlist");
|
|
|
9
9
|
const profiles_1 = require("../config/profiles");
|
|
10
10
|
const envelope_1 = require("../envelope");
|
|
11
11
|
const browser_login_1 = require("../auth/browser-login");
|
|
12
|
+
const refresh_1 = require("../auth/refresh");
|
|
12
13
|
const client_1 = require("../http/client");
|
|
13
14
|
const store_1 = require("../keychain/store");
|
|
14
15
|
const confirmation_1 = require("../safety/confirmation");
|
|
15
16
|
const version_1 = require("../version");
|
|
16
|
-
const run_command_1 = require("../
|
|
17
|
-
const run_command_2 = require("../
|
|
18
|
-
const run_command_3 = require("../
|
|
17
|
+
const run_command_1 = require("../cache/run-command");
|
|
18
|
+
const run_command_2 = require("../engine-backtest/run-command");
|
|
19
|
+
const run_command_3 = require("../resolve-symbols/run-command");
|
|
20
|
+
const run_command_4 = require("../skills/run-command");
|
|
19
21
|
const notify_1 = require("../update/notify");
|
|
20
|
-
const
|
|
22
|
+
const run_command_5 = require("../update/run-command");
|
|
21
23
|
const validate_body_1 = require("../catalog/validate-body");
|
|
22
24
|
const types_1 = require("../install/types");
|
|
23
25
|
const wizard_1 = require("../install/wizard");
|
|
@@ -101,6 +103,7 @@ async function runCli(argv, env = process.env) {
|
|
|
101
103
|
"alphafox api METHOD PATH [--body JSON|--config @file]",
|
|
102
104
|
"alphafox engine-backtest run --experiment <uuid> --definition <id> --config @file --exchange <id> --range FROM..TO --initial-equity N",
|
|
103
105
|
"alphafox engine-backtest sweep --experiment <uuid> --definition <id> --config @file --axes @file --exchange <id> --range FROM..TO --initial-equity N --no-persist",
|
|
106
|
+
"alphafox cache status|clean [--tape|--runtime|--all]",
|
|
104
107
|
"alphafox resolve-symbols <query...> [--exchange binance] [--asset-class equity_perp]",
|
|
105
108
|
"alphafox <domain> <resource> <action> [flags]",
|
|
106
109
|
],
|
|
@@ -119,7 +122,8 @@ async function runCli(argv, env = process.env) {
|
|
|
119
122
|
if (cmd !== "version" &&
|
|
120
123
|
cmd !== "install" &&
|
|
121
124
|
cmd !== "update" &&
|
|
122
|
-
cmd !== "skills"
|
|
125
|
+
cmd !== "skills" &&
|
|
126
|
+
cmd !== "cache") {
|
|
123
127
|
assertCatalogCompatible();
|
|
124
128
|
}
|
|
125
129
|
switch (cmd) {
|
|
@@ -128,9 +132,11 @@ async function runCli(argv, env = process.env) {
|
|
|
128
132
|
case "install":
|
|
129
133
|
return await cmdInstall(args, flags, env);
|
|
130
134
|
case "update":
|
|
131
|
-
return await (0,
|
|
135
|
+
return await (0, run_command_5.cmdUpdate)(args, flags, env);
|
|
132
136
|
case "skills":
|
|
133
|
-
return await (0,
|
|
137
|
+
return await (0, run_command_4.cmdSkills)(args, flags, env);
|
|
138
|
+
case "cache":
|
|
139
|
+
return await (0, run_command_1.cmdCache)(args, flags, env);
|
|
134
140
|
case "doctor":
|
|
135
141
|
return cmdDoctor(flags, env);
|
|
136
142
|
case "whoami":
|
|
@@ -153,7 +159,7 @@ async function runCli(argv, env = process.env) {
|
|
|
153
159
|
sub === "help" ||
|
|
154
160
|
sub === "--help" ||
|
|
155
161
|
sub === "-h") {
|
|
156
|
-
return await (0,
|
|
162
|
+
return await (0, run_command_2.cmdEngineBacktest)(args, flags, env);
|
|
157
163
|
}
|
|
158
164
|
// Hyphen built-in owns `run` and `sweep`. Underscore/hyphen catalog CRUD
|
|
159
165
|
// (engine_backtest.experiments.*) still goes through the typed tree.
|
|
@@ -161,7 +167,7 @@ async function runCli(argv, env = process.env) {
|
|
|
161
167
|
}
|
|
162
168
|
case "resolve-symbols":
|
|
163
169
|
case "resolve-symbol":
|
|
164
|
-
return await (0,
|
|
170
|
+
return await (0, run_command_3.cmdResolveSymbols)(args, flags, env);
|
|
165
171
|
default:
|
|
166
172
|
return await cmdTyped(cmd, args, flags, env);
|
|
167
173
|
}
|
|
@@ -351,15 +357,30 @@ async function cmdAuth(args, flags, env) {
|
|
|
351
357
|
});
|
|
352
358
|
if (sub === "status") {
|
|
353
359
|
const verify = args.includes("--verify");
|
|
354
|
-
|
|
360
|
+
let tokens = (0, store_1.loadTokens)(profile.name, env);
|
|
355
361
|
if (!tokens) {
|
|
356
362
|
(0, envelope_1.writeSuccess)({
|
|
357
363
|
authenticated: false,
|
|
364
|
+
session: "none",
|
|
358
365
|
profile: profile.name,
|
|
359
366
|
verified: false,
|
|
367
|
+
refresh: "no_session",
|
|
368
|
+
accessTokenExpired: null,
|
|
369
|
+
hasRefreshToken: false,
|
|
360
370
|
}, { format: flags.format, jq: flags.jq });
|
|
361
371
|
return 0;
|
|
362
372
|
}
|
|
373
|
+
let refresh = "skipped";
|
|
374
|
+
if ((0, refresh_1.accessTokenNeedsRefresh)(tokens)) {
|
|
375
|
+
const outcome = await (0, refresh_1.refreshStoredTokens)(profile, env);
|
|
376
|
+
refresh = outcome.status;
|
|
377
|
+
if (outcome.status === "refreshed" || outcome.status === "unchanged") {
|
|
378
|
+
tokens = outcome.tokens;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
tokens = (0, store_1.loadTokens)(profile.name, env) ?? tokens;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
363
384
|
let verified = null;
|
|
364
385
|
let whoami = null;
|
|
365
386
|
if (verify) {
|
|
@@ -370,9 +391,18 @@ async function cmdAuth(args, flags, env) {
|
|
|
370
391
|
}, env);
|
|
371
392
|
verified = res.status >= 200 && res.status < 300;
|
|
372
393
|
whoami = verified ? res.json : { status: res.status, body: res.json };
|
|
394
|
+
tokens = (0, store_1.loadTokens)(profile.name, env) ?? tokens;
|
|
373
395
|
}
|
|
396
|
+
const accessTokenExpired = tokens.expiresAt <= Date.now();
|
|
397
|
+
const hasRefreshToken = Boolean(tokens.refreshToken?.trim());
|
|
398
|
+
const session = !accessTokenExpired
|
|
399
|
+
? "active"
|
|
400
|
+
: refresh === "failed"
|
|
401
|
+
? "refresh_failed"
|
|
402
|
+
: "expired";
|
|
374
403
|
(0, envelope_1.writeSuccess)({
|
|
375
|
-
authenticated:
|
|
404
|
+
authenticated: session === "active",
|
|
405
|
+
session,
|
|
376
406
|
profile: profile.name,
|
|
377
407
|
environment: tokens.environment,
|
|
378
408
|
issuer: tokens.issuer,
|
|
@@ -381,6 +411,9 @@ async function cmdAuth(args, flags, env) {
|
|
|
381
411
|
scopes: tokens.scopes,
|
|
382
412
|
accessTokenFingerprint: (0, store_1.tokenFingerprint)(tokens.accessToken),
|
|
383
413
|
expiresAt: tokens.expiresAt,
|
|
414
|
+
accessTokenExpired,
|
|
415
|
+
hasRefreshToken,
|
|
416
|
+
refresh,
|
|
384
417
|
verified,
|
|
385
418
|
whoami,
|
|
386
419
|
}, { format: flags.format, jq: flags.jq });
|
|
@@ -14,6 +14,7 @@ const load_config_1 = require("./load-config");
|
|
|
14
14
|
const parse_args_1 = require("./parse-args");
|
|
15
15
|
const sweep_command_1 = require("./sweep-command");
|
|
16
16
|
const persist_1 = require("./persist");
|
|
17
|
+
const paths_1 = require("../cache/paths");
|
|
17
18
|
const replay_timeframe_1 = require("./replay-timeframe");
|
|
18
19
|
const resolve_packages_1 = require("./resolve-packages");
|
|
19
20
|
var load_config_2 = require("./load-config");
|
|
@@ -316,6 +317,7 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
|
|
|
316
317
|
fromMs: args.range.fromMs,
|
|
317
318
|
toMs: args.range.toMs,
|
|
318
319
|
dataQualityMode: args.dataQualityMode,
|
|
320
|
+
cacheDir: (0, paths_1.resolveTapeCacheDir)(env),
|
|
319
321
|
onProgress: (progress) => {
|
|
320
322
|
emitProgress(flags, writeLine, progress.stage || "tape", progress.fraction, progress.detail);
|
|
321
323
|
},
|
|
@@ -7,6 +7,7 @@ exports.executeEngineBacktestSweep = executeEngineBacktestSweep;
|
|
|
7
7
|
const node_crypto_1 = require("node:crypto");
|
|
8
8
|
const profiles_1 = require("../config/profiles");
|
|
9
9
|
const client_1 = require("../http/client");
|
|
10
|
+
const paths_1 = require("../cache/paths");
|
|
10
11
|
const store_1 = require("../keychain/store");
|
|
11
12
|
const errors_1 = require("./errors");
|
|
12
13
|
const load_config_1 = require("./load-config");
|
|
@@ -189,6 +190,7 @@ async function executeEngineBacktestSweep(args, flags, env = process.env, deps =
|
|
|
189
190
|
fromMs: args.range.fromMs,
|
|
190
191
|
toMs: args.range.toMs,
|
|
191
192
|
dataQualityMode: args.dataQualityMode,
|
|
193
|
+
cacheDir: (0, paths_1.resolveTapeCacheDir)(env),
|
|
192
194
|
onProgress: (progress) => {
|
|
193
195
|
emitProgress(flags, writeLine, progress.stage || "tape", progress.fraction, progress.detail);
|
|
194
196
|
},
|
|
@@ -238,6 +238,7 @@ export interface BacktestRunnerModule {
|
|
|
238
238
|
readonly fromMs: number;
|
|
239
239
|
readonly toMs: number;
|
|
240
240
|
readonly dataQualityMode?: DataQualityMode;
|
|
241
|
+
readonly cacheDir?: string;
|
|
241
242
|
readonly onProgress?: (progress: TapeLoadProgress) => void;
|
|
242
243
|
}, options?: unknown): Promise<TapeLoadResult>;
|
|
243
244
|
assembleScenario(input: {
|
|
@@ -1,141 +1,153 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "@alphafox/cli",
|
|
4
|
-
"packageVersion": "0.3.
|
|
4
|
+
"packageVersion": "0.3.9",
|
|
5
5
|
"contractVersion": "2026-08-13",
|
|
6
|
-
"bundleHash": "
|
|
6
|
+
"bundleHash": "8693bb9b1b6f0c0bd70f75fe564a09ba829726f6e982d05c90a6595e8c36b715",
|
|
7
7
|
"skills": [
|
|
8
8
|
{
|
|
9
9
|
"name": "alphafox",
|
|
10
|
-
"version": "0.3.
|
|
10
|
+
"version": "0.3.9",
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "SKILL.md",
|
|
14
|
-
"sha256": "
|
|
15
|
-
"size":
|
|
14
|
+
"sha256": "cb9f92b75cce3a03d4f2a9492b6ddd564541828bdb74acaf360cb3f2a0aa8447",
|
|
15
|
+
"size": 4405
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
|
-
"hash": "
|
|
18
|
+
"hash": "92b330a3f902405235e2a049ce6c98a7cad705f8aeab6ba3c2062df33b222aec"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"name": "alphafox-account",
|
|
22
|
-
"version": "0.3.
|
|
22
|
+
"version": "0.3.9",
|
|
23
23
|
"files": [
|
|
24
24
|
{
|
|
25
25
|
"path": "SKILL.md",
|
|
26
|
-
"sha256": "
|
|
26
|
+
"sha256": "4ea9c43c579ce7b4a4e5dd0e0ca10eeb793d19fc64e1cf8c4760b6d426524bf6",
|
|
27
27
|
"size": 783
|
|
28
28
|
}
|
|
29
29
|
],
|
|
30
|
-
"hash": "
|
|
30
|
+
"hash": "a94dc7e2e7a958394dbebba5b02e25c7114116b0b1a265be6b544e92f5e4ea65"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"name": "alphafox-admin",
|
|
34
|
-
"version": "0.3.
|
|
34
|
+
"version": "0.3.9",
|
|
35
35
|
"files": [
|
|
36
36
|
{
|
|
37
37
|
"path": "SKILL.md",
|
|
38
|
-
"sha256": "
|
|
38
|
+
"sha256": "6c800c87dd98fc054508ffc179fce19f455cc2da762525accadf2b1fda68e647",
|
|
39
39
|
"size": 787
|
|
40
40
|
}
|
|
41
41
|
],
|
|
42
|
-
"hash": "
|
|
42
|
+
"hash": "534d66e2614677fa7e40d3334f2e6992c632dccffc623c9dfcd963ae49eb66ac"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"name": "alphafox-auth",
|
|
46
|
-
"version": "0.3.
|
|
46
|
+
"version": "0.3.9",
|
|
47
47
|
"files": [
|
|
48
48
|
{
|
|
49
49
|
"path": "SKILL.md",
|
|
50
|
-
"sha256": "
|
|
51
|
-
"size":
|
|
50
|
+
"sha256": "a12ab3e613b26246ef6eb8d1289307c266d3c82861a8d6567856d63dff47cd6d",
|
|
51
|
+
"size": 2370
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
|
-
"hash": "
|
|
54
|
+
"hash": "e4b6b6f1dc76681e37c1a71ecaa10ab985b4dcc04ca5b2eca8bfcec367076a02"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "alphafox-cache",
|
|
58
|
+
"version": "0.3.9",
|
|
59
|
+
"files": [
|
|
60
|
+
{
|
|
61
|
+
"path": "SKILL.md",
|
|
62
|
+
"sha256": "cceafb6d8d0017090eedb5be82dd13fe2c26963531c5e92f49d1ed7ba948fc5b",
|
|
63
|
+
"size": 1529
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"hash": "f858ee9c3e27c56035cede0f7a6929fc5128573a718762802fba67b7c9d6103a"
|
|
55
67
|
},
|
|
56
68
|
{
|
|
57
69
|
"name": "alphafox-engine-backtest",
|
|
58
|
-
"version": "0.3.
|
|
70
|
+
"version": "0.3.9",
|
|
59
71
|
"files": [
|
|
60
72
|
{
|
|
61
73
|
"path": "SKILL.md",
|
|
62
|
-
"sha256": "
|
|
63
|
-
"size":
|
|
74
|
+
"sha256": "8751aa7218862c44b4635d72a02a31752e400a67b41adec03aea77678a087c48",
|
|
75
|
+
"size": 7802
|
|
64
76
|
}
|
|
65
77
|
],
|
|
66
|
-
"hash": "
|
|
78
|
+
"hash": "2bab573fa07f5c5c1d83eb2e66d96d06abbff723e62f6b84a2de30a6ccf42339"
|
|
67
79
|
},
|
|
68
80
|
{
|
|
69
81
|
"name": "alphafox-exchange",
|
|
70
|
-
"version": "0.3.
|
|
82
|
+
"version": "0.3.9",
|
|
71
83
|
"files": [
|
|
72
84
|
{
|
|
73
85
|
"path": "SKILL.md",
|
|
74
|
-
"sha256": "
|
|
86
|
+
"sha256": "416999214b3e99a9bbb3653286b2eefadbb7721da00762921e2c702de17f112a",
|
|
75
87
|
"size": 743
|
|
76
88
|
}
|
|
77
89
|
],
|
|
78
|
-
"hash": "
|
|
90
|
+
"hash": "988bdad202ba2e522d5d025339ed354063ada77ae56b0d2616963e13df659d83"
|
|
79
91
|
},
|
|
80
92
|
{
|
|
81
93
|
"name": "alphafox-market",
|
|
82
|
-
"version": "0.3.
|
|
94
|
+
"version": "0.3.9",
|
|
83
95
|
"files": [
|
|
84
96
|
{
|
|
85
97
|
"path": "SKILL.md",
|
|
86
|
-
"sha256": "
|
|
98
|
+
"sha256": "5181ca01a222ae17c3e8feb711211c4150f59745b1559da5537a5b42ff4439b9",
|
|
87
99
|
"size": 3078
|
|
88
100
|
}
|
|
89
101
|
],
|
|
90
|
-
"hash": "
|
|
102
|
+
"hash": "29f11a29419e5443593255e07bc6055fe3d15e165ef5e442c550f56845dcb12d"
|
|
91
103
|
},
|
|
92
104
|
{
|
|
93
105
|
"name": "alphafox-notification",
|
|
94
|
-
"version": "0.3.
|
|
106
|
+
"version": "0.3.9",
|
|
95
107
|
"files": [
|
|
96
108
|
{
|
|
97
109
|
"path": "SKILL.md",
|
|
98
|
-
"sha256": "
|
|
110
|
+
"sha256": "0f027ae12e13832a2d267bdf276a59882dc42396d45d31e6a43424d514209906",
|
|
99
111
|
"size": 698
|
|
100
112
|
}
|
|
101
113
|
],
|
|
102
|
-
"hash": "
|
|
114
|
+
"hash": "f8aa26444dd99e3374999086ab2956ce82156de2aab4c40a97a654082f899a29"
|
|
103
115
|
},
|
|
104
116
|
{
|
|
105
117
|
"name": "alphafox-shared",
|
|
106
|
-
"version": "0.3.
|
|
118
|
+
"version": "0.3.9",
|
|
107
119
|
"files": [
|
|
108
120
|
{
|
|
109
121
|
"path": "SKILL.md",
|
|
110
|
-
"sha256": "
|
|
111
|
-
"size":
|
|
122
|
+
"sha256": "896e71ca6da5f82b9c857486be8225a623a2e3a01be1a807db4d21d209c69a18",
|
|
123
|
+
"size": 5385
|
|
112
124
|
}
|
|
113
125
|
],
|
|
114
|
-
"hash": "
|
|
126
|
+
"hash": "fc69c60e86b01d917f0a513d20643331d29ed2a7baa15577cd19f049b8f8b24a"
|
|
115
127
|
},
|
|
116
128
|
{
|
|
117
129
|
"name": "alphafox-strategy",
|
|
118
|
-
"version": "0.3.
|
|
130
|
+
"version": "0.3.9",
|
|
119
131
|
"files": [
|
|
120
132
|
{
|
|
121
133
|
"path": "SKILL.md",
|
|
122
|
-
"sha256": "
|
|
134
|
+
"sha256": "bbdba03e06c56e5fd35700071841e04a45c9b04d43727ba599bfbecf2fd77e1e",
|
|
123
135
|
"size": 1826
|
|
124
136
|
}
|
|
125
137
|
],
|
|
126
|
-
"hash": "
|
|
138
|
+
"hash": "fb5847c1d8d0229d7b0d3598dbb26e1104be250dfe3a62d1759354c3ecba3e3d"
|
|
127
139
|
},
|
|
128
140
|
{
|
|
129
141
|
"name": "alphafox-trading",
|
|
130
|
-
"version": "0.3.
|
|
142
|
+
"version": "0.3.9",
|
|
131
143
|
"files": [
|
|
132
144
|
{
|
|
133
145
|
"path": "SKILL.md",
|
|
134
|
-
"sha256": "
|
|
146
|
+
"sha256": "454102427cbb7ec119265c0c38adc9acd822881e23fad725b49f693c32ed19e3",
|
|
135
147
|
"size": 3122
|
|
136
148
|
}
|
|
137
149
|
],
|
|
138
|
-
"hash": "
|
|
150
|
+
"hash": "4768ca40fba0d940bf8425dc544f9501c9c5bca5a920fc942247eb7557ec9dcf"
|
|
139
151
|
}
|
|
140
152
|
]
|
|
141
153
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -3,6 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CLI_CONTRACT_VERSION = exports.CLI_VERSION = exports.CLI_PACKAGE = exports.CLI_NAME = void 0;
|
|
4
4
|
exports.CLI_NAME = "alphafox";
|
|
5
5
|
exports.CLI_PACKAGE = "@alphafox/cli";
|
|
6
|
-
exports.CLI_VERSION = "0.3.
|
|
6
|
+
exports.CLI_VERSION = "0.3.9";
|
|
7
7
|
var operations_1 = require("./catalog/operations");
|
|
8
8
|
Object.defineProperty(exports, "CLI_CONTRACT_VERSION", { enumerable: true, get: function () { return operations_1.CATALOG_VERSION; } });
|
package/package.json
CHANGED
package/skills/account/SKILL.md
CHANGED
package/skills/admin/SKILL.md
CHANGED
package/skills/alphafox/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox
|
|
3
|
-
description: AlphaFox CLI entry router. Use for any AlphaFox request — install, update, login, whoami, 回测, engine backtest, strategy definitions, create/list/start/stop a running strategy (trader), ticker/标的 resolve (美股 or crypto), market data, exchange connectors, wallet, subscriptions, notifications, or admin. If a CLI command prints `[alphafox] update available`, ask the user「检测到新的版本,是否需要我帮你升级?」and only then run `alphafox update --format json --no-input`. Start here, then open the routed domain skill. Do not guess alphafox-engine-backtest vs alphafox-strategy vs alphafox-trading from memory.
|
|
4
|
-
version: 0.3.
|
|
3
|
+
description: AlphaFox CLI entry router. Use for any AlphaFox request — install, update, login, whoami, 回测, engine backtest, 清理回测缓存 / 历史数据, strategy definitions, create/list/start/stop a running strategy (trader), ticker/标的 resolve (美股 or crypto), market data, exchange connectors, wallet, subscriptions, notifications, or admin. If a CLI command prints `[alphafox] update available`, ask the user「检测到新的版本,是否需要我帮你升级?」and only then run `alphafox update --format json --no-input`. After a large backtest, if tape cache is large, ask「回测下载的历史数据比较大,要不要我帮你清理本地缓存?」then open `alphafox-cache`. Start here, then open the routed domain skill. Do not guess alphafox-engine-backtest vs alphafox-strategy vs alphafox-trading from memory.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# AlphaFox
|
|
@@ -22,6 +22,7 @@ A **trader** is a running strategy instance (paper or live), not a person. Creat
|
|
|
22
22
|
| Login, logout, whoami, profile, staging vs production | `alphafox-auth` |
|
|
23
23
|
| Ticker / 标的 / 美股 / crypto / resolve a misspelled symbol | `alphafox-market` |
|
|
24
24
|
| Engine WASM backtest, experiment, `engine-backtest run`, persist a local run | `alphafox-engine-backtest` |
|
|
25
|
+
| 清理回测缓存 / 历史 K 线占磁盘 / `alphafox cache` | `alphafox-cache` |
|
|
25
26
|
| Strategy types / definitions / validate config (grid, dca, copy, …) | `alphafox-strategy` |
|
|
26
27
|
| Create, list, start, or stop a running strategy (trader), including copy | `alphafox-trading` |
|
|
27
28
|
| Exchange connectors | `alphafox-exchange` |
|
|
@@ -36,7 +37,7 @@ If several rows apply, load **all** of them (typical: `alphafox-shared` + `alpha
|
|
|
36
37
|
The CLI may print this on **stderr** at most once every 24 hours:
|
|
37
38
|
|
|
38
39
|
```text
|
|
39
|
-
[alphafox] update available: 0.3.
|
|
40
|
+
[alphafox] update available: 0.3.8 -> 0.3.9. After the user confirms, run: alphafox update --format json --no-input,
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
If you see that notice (or `updateAvailable: true` from `alphafox update --check`):
|
|
@@ -60,3 +61,13 @@ Do not install Skills from GitHub. Details and dry-run / check commands live in
|
|
|
60
61
|
- Web `/api/v1/backtests` (`backtests.*`) is **not** a CLI surface. Do not call it via typed commands, `schema`, or `alphafox api`.
|
|
61
62
|
|
|
62
63
|
Ambiguous “帮我回测” → `alphafox-engine-backtest`, after resolving symbols.
|
|
64
|
+
|
|
65
|
+
## Large historical tape
|
|
66
|
+
|
|
67
|
+
`engine-backtest run|sweep` downloads closed OHLCV into the local tape cache. After a long-range or 1m backtest (or whenever the operator mentions disk / 缓存), read `alphafox-cache` and run `alphafox cache status --format json --no-input`.
|
|
68
|
+
|
|
69
|
+
If `data.tape.large` is true (tape ≥ `data.remindAfterBytes`):
|
|
70
|
+
|
|
71
|
+
1. Ask the user: **回测下载的历史数据比较大,要不要我帮你清理本地缓存?**
|
|
72
|
+
2. Wait for an explicit yes. Do not clean on your own.
|
|
73
|
+
3. Follow `alphafox-cache` (`alphafox cache clean --dry-run`, then `--yes`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-shared
|
|
3
3
|
description: Shared AlphaFox CLI rules for Agents — auth, profiles, envelopes, risk gates, and public operationIds only.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# AlphaFox shared Agent contract
|
|
@@ -67,9 +67,10 @@ alphafox auth login --no-wait --format json --no-input
|
|
|
67
67
|
# show verification_uri / user_code to the human, then:
|
|
68
68
|
alphafox auth login --device-code <device_code> --format json --no-input
|
|
69
69
|
alphafox auth status --verify --format json --no-input
|
|
70
|
-
alphafox whoami --format json --no-input
|
|
71
70
|
```
|
|
72
71
|
|
|
72
|
+
Access tokens last ~10 minutes; the CLI refreshes them. After idle, run **one** `auth status --verify` — not `whoami` in parallel. `session: active` means logged in. A past `expiresAt` is not logout. Re-login only when `session` is `none` or `refresh_failed`.
|
|
73
|
+
|
|
73
74
|
Local browser: `alphafox auth login --browser --format json --no-input` (loopback 127.0.0.1). If the browser cannot open, copy `authorizeUrl` from the error; do not invent a Device Flow retry unless the operator is headless.
|
|
74
75
|
|
|
75
76
|
Wrong environment / missing permission / missing `--yes`: stop. Do not retry with a different profile.
|
package/skills/auth/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-auth
|
|
3
3
|
description: Login, status, logout, whoami, and environment isolation for AlphaFox CLI.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Auth Skill
|
|
@@ -19,23 +19,26 @@ Always `--format json --no-input`. Never `--token`.
|
|
|
19
19
|
1. `alphafox auth login --no-wait --format json --no-input`
|
|
20
20
|
2. Present `verification_uri` / `user_code` to the human.
|
|
21
21
|
3. After approval: `alphafox auth login --device-code <device_code> --format json --no-input`
|
|
22
|
-
4. `alphafox
|
|
22
|
+
4. `alphafox auth status --verify --format json --no-input` (do not also run `whoami` in parallel)
|
|
23
23
|
|
|
24
24
|
### Browser loopback (human, local machine)
|
|
25
25
|
|
|
26
26
|
1. `alphafox auth login --browser --format json --no-input`
|
|
27
27
|
2. CLI binds `127.0.0.1` and opens the system browser. Do not copy codes or verifiers.
|
|
28
|
-
3. After the localhost callback, `alphafox
|
|
28
|
+
3. After the localhost callback, `alphafox auth status --verify --format json --no-input`
|
|
29
29
|
4. If the browser cannot open, the error includes a copyable `authorizeUrl`. Do not retry as Device Flow unless the human is headless.
|
|
30
30
|
|
|
31
31
|
### Status / logout
|
|
32
32
|
|
|
33
|
-
- `alphafox auth status --verify --format json --no-input`
|
|
33
|
+
- `alphafox auth status --verify --format json --no-input` is enough. Do **not** also run `whoami` in parallel — concurrent refresh can kill the session.
|
|
34
|
+
- Access tokens last ~10 minutes. The CLI refreshes them automatically. A past `expiresAt` is **not** logout.
|
|
35
|
+
- Logged in: `session` is `active` (or `authenticated: true` after status). Re-login only when `session` is `none` or `refresh_failed`.
|
|
34
36
|
- `alphafox auth logout --format json --no-input` (server revoke + local keychain clear). If `remoteRevoke` is `failed`, local tokens are still cleared but exit is non-zero — do not claim a full logout.
|
|
35
37
|
|
|
36
38
|
### Recovery
|
|
37
39
|
|
|
38
|
-
- `
|
|
40
|
+
- `session: refresh_failed` / refresh grant `invalid_grant`: re-run Device Flow or browser login. Do not reuse a token from another profile.
|
|
41
|
+
- Do not treat a short idle or an expired access token as a missing login.
|
|
39
42
|
- Cross-env: production tokens are rejected on staging/local. Switch `--profile` only with explicit operator intent.
|
|
40
43
|
|
|
41
44
|
## Safety
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alphafox-cache
|
|
3
|
+
description: Inspect and clean local Engine backtest caches (downloaded OHLCV tape and wasm runtime). Use when the user asks to 清理缓存, free disk, or after a large historical backtest.
|
|
4
|
+
version: 0.3.9
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Cache
|
|
8
|
+
|
|
9
|
+
Always `--format json --no-input`. Never `--token`. This is local disk only.
|
|
10
|
+
|
|
11
|
+
`engine-backtest run|sweep` writes closed OHLCV into the **tape** cache (`~/.alphafox/cache/engine-backtest`, or `ALPHAFOX_TAPE_CACHE_DIR`). The wasm / Node host lives under the **runtime** cache (`~/.cache/alphafox/engine-backtest/<hash>/`). Tape is the large historical download.
|
|
12
|
+
|
|
13
|
+
## Status first
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
alphafox cache status --format json --no-input
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Read `data.tape.bytes`, `data.tape.files`, `data.tape.large`, `data.remindAfterBytes`. `large` is true when tape bytes ≥ `remindAfterBytes` (512 MiB).
|
|
20
|
+
|
|
21
|
+
If `data.tape.large` is true, ask the user:
|
|
22
|
+
|
|
23
|
+
**回测下载的历史数据比较大,要不要我帮你清理本地缓存?**
|
|
24
|
+
|
|
25
|
+
Wait for an explicit yes. Do not clean on your own.
|
|
26
|
+
|
|
27
|
+
## Clean
|
|
28
|
+
|
|
29
|
+
Default clean is **tape only** (historical bars). Runtime re-downloads on the next run; only add `--runtime` or `--all` when the operator asks.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
alphafox cache clean --dry-run --format json --no-input
|
|
33
|
+
alphafox cache clean --yes --format json --no-input
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`--yes` is required to delete. `--dry-run` reports what would be removed. After clean, `data.bytesFreed` is the space recovered.
|
|
37
|
+
|
|
38
|
+
Do not `rm` cache paths by hand. Do not delete `~/.config/alphafox` (config / skills state / keychain).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-engine-backtest
|
|
3
3
|
description: Local Engine WASM backtest (alphafox engine-backtest run|sweep) vs catalog experiment CRUD.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Engine Backtest
|
|
@@ -94,6 +94,7 @@ Owner isolation and 7-day expiry are enforced by the server. Applying a coordina
|
|
|
94
94
|
2. `engine-backtest run` (reuse `--experiment` after the first create).
|
|
95
95
|
3. Read `data.metrics` / `data.engineVersion` / `data.runId` / `data.experimentUrl`.
|
|
96
96
|
4. Adjust parameters and run again. Do not invent a token flag if persist returns 401 — `alphafox auth login`.
|
|
97
|
+
5. After a long-range or 1m run, follow `alphafox-cache`: `alphafox cache status`. If `data.tape.large` is true, ask **回测下载的历史数据比较大,要不要我帮你清理本地缓存?** and wait for yes.
|
|
97
98
|
|
|
98
99
|
## Safety
|
|
99
100
|
|
package/skills/exchange/SKILL.md
CHANGED
package/skills/market/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-market
|
|
3
3
|
description: Market data and ticker resolution for US equity perps, RWAs, and crypto on the same perp catalog. Use when the user names 美股, NVDA, AAPL, BTC, or any 标的. Keep the operator's asset class via symbolMetadata — do not rewrite NVDA into a crypto coin.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Market
|
package/skills/strategy/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alphafox-strategy
|
|
3
3
|
description: Strategy definitions — list types (grid, dca, copy, …) and validate config. Creating a running strategy is creating a trader; use alphafox-trading for that.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Strategy definitions
|
package/skills/trading/SKILL.md
CHANGED
|
@@ -288,6 +288,8 @@ export interface TapeLoadRequest {
|
|
|
288
288
|
readonly signal?: AbortSignal;
|
|
289
289
|
readonly onProgress?: (progress: TapeLoadProgress) => void;
|
|
290
290
|
readonly nowMs?: number;
|
|
291
|
+
readonly cacheDir?: string;
|
|
292
|
+
readonly seriesConcurrency?: number;
|
|
291
293
|
readonly ohlcvFetcher?: TapeOhlcvFetcher;
|
|
292
294
|
readonly fundingFetcher?: TapeFundingFetcher;
|
|
293
295
|
readonly marketsLoader?: () => Promise<TapeMarketsSnapshot>;
|
|
@@ -302,6 +304,8 @@ export interface TapeProxyOptions {
|
|
|
302
304
|
export interface TapeLoadOptions extends TapeProxyOptions {
|
|
303
305
|
readonly cache?: FileTapeCache | false | "disable";
|
|
304
306
|
readonly cacheDir?: string;
|
|
307
|
+
/** Independent series fetches; default 4, hard cap 8. Pagination stays serial. */
|
|
308
|
+
readonly seriesConcurrency?: number;
|
|
305
309
|
readonly nowMs?: number;
|
|
306
310
|
readonly onProgress?: (progress: TapeLoadProgress) => void;
|
|
307
311
|
readonly createExchange?: (
|
|
@@ -336,6 +340,17 @@ export interface TapeLoadResult {
|
|
|
336
340
|
};
|
|
337
341
|
}
|
|
338
342
|
|
|
343
|
+
export const DEFAULT_TAPE_SERIES_CONCURRENCY: 4;
|
|
344
|
+
export const MAX_TAPE_SERIES_CONCURRENCY: 8;
|
|
345
|
+
|
|
346
|
+
export function resolveTapeSeriesConcurrency(value?: number): number;
|
|
347
|
+
|
|
348
|
+
export function mapWithConcurrency<T, R>(
|
|
349
|
+
items: readonly T[],
|
|
350
|
+
concurrency: number,
|
|
351
|
+
worker: (item: T, index: number) => Promise<R>
|
|
352
|
+
): Promise<R[]>;
|
|
353
|
+
|
|
339
354
|
export function loadTape(
|
|
340
355
|
request: TapeLoadRequest,
|
|
341
356
|
options?: TapeLoadOptions
|
|
@@ -50,8 +50,12 @@ export {
|
|
|
50
50
|
loadSeriesWithCache,
|
|
51
51
|
} from "./lib/series.mjs";
|
|
52
52
|
export {
|
|
53
|
+
DEFAULT_TAPE_SERIES_CONCURRENCY,
|
|
54
|
+
MAX_TAPE_SERIES_CONCURRENCY,
|
|
53
55
|
loadTape,
|
|
56
|
+
mapWithConcurrency,
|
|
54
57
|
resolveTapeCache,
|
|
58
|
+
resolveTapeSeriesConcurrency,
|
|
55
59
|
effectiveTapeEndMs,
|
|
56
60
|
inferFundingIntervals,
|
|
57
61
|
classifyTapeSymbolsForPreflight,
|
|
@@ -39,8 +39,47 @@ const FUNDING_INTERVALS = [
|
|
|
39
39
|
{ interval: "8h", spacingMs: 28_800_000 },
|
|
40
40
|
];
|
|
41
41
|
|
|
42
|
+
/** Independent symbol×timeframe (and funding) fetches. Pagination stays serial. */
|
|
43
|
+
export const DEFAULT_TAPE_SERIES_CONCURRENCY = 4;
|
|
44
|
+
export const MAX_TAPE_SERIES_CONCURRENCY = 8;
|
|
45
|
+
|
|
42
46
|
const exchangePromises = new Map();
|
|
43
47
|
|
|
48
|
+
export function resolveTapeSeriesConcurrency(value) {
|
|
49
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 1) {
|
|
50
|
+
return Math.min(
|
|
51
|
+
MAX_TAPE_SERIES_CONCURRENCY,
|
|
52
|
+
Math.max(1, Math.floor(value))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return DEFAULT_TAPE_SERIES_CONCURRENCY;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function mapWithConcurrency(items, concurrency, worker) {
|
|
59
|
+
const list = [...items];
|
|
60
|
+
if (list.length === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const limit = Math.min(
|
|
64
|
+
list.length,
|
|
65
|
+
Math.max(1, Math.floor(Number(concurrency)) || 1)
|
|
66
|
+
);
|
|
67
|
+
const results = new Array(list.length);
|
|
68
|
+
let nextIndex = 0;
|
|
69
|
+
async function runWorker() {
|
|
70
|
+
while (true) {
|
|
71
|
+
const index = nextIndex;
|
|
72
|
+
nextIndex += 1;
|
|
73
|
+
if (index >= list.length) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
results[index] = await worker(list[index], index);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
export function effectiveTapeEndMs(
|
|
45
84
|
requestedToMs,
|
|
46
85
|
nowMs = Date.now(),
|
|
@@ -141,7 +180,13 @@ export async function loadTape(request, options = {}) {
|
|
|
141
180
|
|
|
142
181
|
const exchangeDefinition = resolveRequestExchange(request);
|
|
143
182
|
const onProgress = request.onProgress ?? options.onProgress;
|
|
144
|
-
const cache = resolveTapeCache(
|
|
183
|
+
const cache = resolveTapeCache({
|
|
184
|
+
...options,
|
|
185
|
+
cacheDir: options.cacheDir ?? request.cacheDir,
|
|
186
|
+
});
|
|
187
|
+
const seriesConcurrency = resolveTapeSeriesConcurrency(
|
|
188
|
+
options.seriesConcurrency ?? request.seriesConcurrency
|
|
189
|
+
);
|
|
145
190
|
const dataQualityMode = request.dataQualityMode ?? "strict";
|
|
146
191
|
const baseTimeframe = resolvePlanBaseTimeframe({
|
|
147
192
|
baseTimeframe: request.baseTimeframe,
|
|
@@ -233,17 +278,36 @@ export async function loadTape(request, options = {}) {
|
|
|
233
278
|
const buffers = {};
|
|
234
279
|
const chartSeries = [];
|
|
235
280
|
const series = [];
|
|
236
|
-
const
|
|
281
|
+
const seriesJobs = request.symbols.flatMap((symbol) =>
|
|
282
|
+
timeframes.map((timeframe) => ({ symbol, timeframe }))
|
|
283
|
+
);
|
|
284
|
+
const totalSeries = seriesJobs.length;
|
|
237
285
|
const dataIssues = [];
|
|
238
286
|
const coverageWarnings = [];
|
|
239
|
-
|
|
240
|
-
let
|
|
241
|
-
|
|
242
|
-
|
|
287
|
+
const seriesFractions = new Array(totalSeries).fill(0);
|
|
288
|
+
let lastOhlcvDetail = "";
|
|
289
|
+
const reportOhlcv = (index, fraction, detail) => {
|
|
290
|
+
seriesFractions[index] = fraction;
|
|
291
|
+
lastOhlcvDetail = detail;
|
|
292
|
+
const completed =
|
|
293
|
+
seriesFractions.reduce((sum, value) => sum + value, 0) / totalSeries;
|
|
294
|
+
onProgress?.({
|
|
295
|
+
stage: "ohlcv",
|
|
296
|
+
detail: lastOhlcvDetail,
|
|
297
|
+
fraction:
|
|
298
|
+
MARKETS_PROGRESS_END +
|
|
299
|
+
(OHLCV_PROGRESS_END - MARKETS_PROGRESS_END) * completed,
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
const loadedSeries = await mapWithConcurrency(
|
|
303
|
+
seriesJobs,
|
|
304
|
+
seriesConcurrency,
|
|
305
|
+
async (job, index) => {
|
|
243
306
|
request.signal?.throwIfAborted();
|
|
244
|
-
|
|
307
|
+
const { symbol, timeframe } = job;
|
|
308
|
+
const detail = `${symbol} ${timeframe}`;
|
|
245
309
|
try {
|
|
246
|
-
loaded = await loadSeriesWithCache(
|
|
310
|
+
const loaded = await loadSeriesWithCache(
|
|
247
311
|
exchange,
|
|
248
312
|
exchangeDefinition,
|
|
249
313
|
runtimeConfig,
|
|
@@ -255,46 +319,51 @@ export async function loadTape(request, options = {}) {
|
|
|
255
319
|
requirementWarmups.get(`${symbol}\u0000${timeframe}`) ?? 0,
|
|
256
320
|
timeframe === baseTimeframe,
|
|
257
321
|
dataQualityMode,
|
|
258
|
-
(fraction) =>
|
|
259
|
-
onProgress?.({
|
|
260
|
-
stage: "ohlcv",
|
|
261
|
-
detail: `${symbol} ${timeframe}`,
|
|
262
|
-
fraction:
|
|
263
|
-
MARKETS_PROGRESS_END +
|
|
264
|
-
(OHLCV_PROGRESS_END - MARKETS_PROGRESS_END) *
|
|
265
|
-
((seriesDone + fraction) / totalSeries),
|
|
266
|
-
});
|
|
267
|
-
},
|
|
322
|
+
(fraction) => reportOhlcv(index, fraction, detail),
|
|
268
323
|
cacheUntilMs,
|
|
269
324
|
cache,
|
|
270
325
|
request.signal
|
|
271
326
|
);
|
|
327
|
+
reportOhlcv(index, 1, detail);
|
|
328
|
+
return { ok: true, job, loaded };
|
|
272
329
|
} catch (error) {
|
|
273
330
|
request.signal?.throwIfAborted();
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
coverageWarnings.push(
|
|
280
|
-
formatCoverageSoftWarning(loaded.softIssues, loaded.coverageRatio)
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
const { rows } = loaded;
|
|
284
|
-
const bufferKey = `k${bufferSequence++}`;
|
|
285
|
-
const buffer = encodeOhlcvColumns(rows);
|
|
286
|
-
buffers[bufferKey] = buffer;
|
|
287
|
-
if (timeframe === baseTimeframe) {
|
|
288
|
-
chartSeries.push({
|
|
289
|
-
symbol,
|
|
290
|
-
timeframe: baseTimeframe,
|
|
291
|
-
rows: rows.length,
|
|
292
|
-
buffer: buffer.slice(0),
|
|
293
|
-
});
|
|
331
|
+
reportOhlcv(index, 1, detail);
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
issues: toTapeDataIssues(error, symbol, timeframe),
|
|
335
|
+
};
|
|
294
336
|
}
|
|
295
|
-
series.push({ symbol, timeframe, buffer: bufferKey, rows: rows.length });
|
|
296
|
-
seriesDone++;
|
|
297
337
|
}
|
|
338
|
+
);
|
|
339
|
+
let bufferSequence = 0;
|
|
340
|
+
for (const result of loadedSeries) {
|
|
341
|
+
if (!result.ok) {
|
|
342
|
+
dataIssues.push(...result.issues);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (result.loaded.softIssues.length > 0) {
|
|
346
|
+
coverageWarnings.push(
|
|
347
|
+
formatCoverageSoftWarning(
|
|
348
|
+
result.loaded.softIssues,
|
|
349
|
+
result.loaded.coverageRatio
|
|
350
|
+
)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const { symbol, timeframe } = result.job;
|
|
354
|
+
const { rows } = result.loaded;
|
|
355
|
+
const bufferKey = `k${bufferSequence++}`;
|
|
356
|
+
const buffer = encodeOhlcvColumns(rows);
|
|
357
|
+
buffers[bufferKey] = buffer;
|
|
358
|
+
if (timeframe === baseTimeframe) {
|
|
359
|
+
chartSeries.push({
|
|
360
|
+
symbol,
|
|
361
|
+
timeframe: baseTimeframe,
|
|
362
|
+
rows: rows.length,
|
|
363
|
+
buffer: buffer.slice(0),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
series.push({ symbol, timeframe, buffer: bufferKey, rows: rows.length });
|
|
298
367
|
}
|
|
299
368
|
if (dataIssues.length > 0) {
|
|
300
369
|
throw new TapeDataUnavailableError(dataIssues);
|
|
@@ -302,19 +371,24 @@ export async function loadTape(request, options = {}) {
|
|
|
302
371
|
|
|
303
372
|
let fundingRates;
|
|
304
373
|
if (request.needsFunding) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
374
|
+
const fundingEntries = await mapWithConcurrency(
|
|
375
|
+
request.symbols,
|
|
376
|
+
seriesConcurrency,
|
|
377
|
+
async (symbol) => {
|
|
378
|
+
request.signal?.throwIfAborted();
|
|
379
|
+
const samples = await loadFundingHistory(
|
|
380
|
+
exchange,
|
|
381
|
+
exchangeDefinition,
|
|
382
|
+
runtimeConfig,
|
|
383
|
+
symbol,
|
|
384
|
+
request.fromMs,
|
|
385
|
+
tapeToMs,
|
|
386
|
+
request.signal
|
|
387
|
+
);
|
|
388
|
+
return [symbol, samples];
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
fundingRates = Object.fromEntries(fundingEntries);
|
|
318
392
|
}
|
|
319
393
|
request.signal?.throwIfAborted();
|
|
320
394
|
onProgress?.({
|