@alphafox/cli 0.3.6 → 0.3.8
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/catalog/omit.d.ts +3 -1
- package/dist/catalog/omit.js +9 -2
- package/dist/commands/run.js +31 -3
- package/dist/resolve-symbols/catalog.d.ts +6 -0
- package/dist/resolve-symbols/catalog.js +43 -0
- package/dist/resolve-symbols/exchanges.js +7 -4
- package/dist/resolve-symbols/match.d.ts +4 -3
- package/dist/resolve-symbols/match.js +50 -48
- package/dist/resolve-symbols/parse-args.d.ts +1 -0
- package/dist/resolve-symbols/parse-args.js +20 -3
- package/dist/resolve-symbols/run-command.js +19 -6
- package/dist/resolve-symbols/types.d.ts +14 -0
- package/dist/skills-manifest.json +42 -42
- 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 +11 -9
- package/skills/alphafox-shared/SKILL.md +5 -4
- package/skills/auth/SKILL.md +8 -5
- package/skills/engine-backtest/SKILL.md +5 -5
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +32 -12
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +17 -15
- package/skills/trading/SKILL.md +37 -7
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`;
|
package/dist/catalog/omit.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Not a CLI surface:
|
|
3
|
+
* - Web `/api/v1/backtests` jobs (`backtests` / `backtests.*`)
|
|
4
|
+
* - Chat product (`chats` / `chats.*`, `chat_summaries` / `chat_summaries.*`)
|
|
3
5
|
* Local Engine WASM (`engine-backtest run`) and `engine_backtest.*` stay.
|
|
4
6
|
* Match `backtests` / `backtests.*` only — never `engine_backtest.*`.
|
|
5
7
|
*/
|
package/dist/catalog/omit.js
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isOmittedCatalogOperation = isOmittedCatalogOperation;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Not a CLI surface:
|
|
6
|
+
* - Web `/api/v1/backtests` jobs (`backtests` / `backtests.*`)
|
|
7
|
+
* - Chat product (`chats` / `chats.*`, `chat_summaries` / `chat_summaries.*`)
|
|
6
8
|
* Local Engine WASM (`engine-backtest run`) and `engine_backtest.*` stay.
|
|
7
9
|
* Match `backtests` / `backtests.*` only — never `engine_backtest.*`.
|
|
8
10
|
*/
|
|
9
11
|
function isOmittedCatalogOperation(operationId) {
|
|
10
|
-
return operationId === "backtests" ||
|
|
12
|
+
return (operationId === "backtests" ||
|
|
13
|
+
operationId.startsWith("backtests.") ||
|
|
14
|
+
operationId === "chats" ||
|
|
15
|
+
operationId.startsWith("chats.") ||
|
|
16
|
+
operationId === "chat_summaries" ||
|
|
17
|
+
operationId.startsWith("chat_summaries."));
|
|
11
18
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -9,6 +9,7 @@ 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");
|
|
@@ -101,7 +102,7 @@ async function runCli(argv, env = process.env) {
|
|
|
101
102
|
"alphafox api METHOD PATH [--body JSON|--config @file]",
|
|
102
103
|
"alphafox engine-backtest run --experiment <uuid> --definition <id> --config @file --exchange <id> --range FROM..TO --initial-equity N",
|
|
103
104
|
"alphafox engine-backtest sweep --experiment <uuid> --definition <id> --config @file --axes @file --exchange <id> --range FROM..TO --initial-equity N --no-persist",
|
|
104
|
-
"alphafox resolve-symbols <query...> [--exchange binance]",
|
|
105
|
+
"alphafox resolve-symbols <query...> [--exchange binance] [--asset-class equity_perp]",
|
|
105
106
|
"alphafox <domain> <resource> <action> [flags]",
|
|
106
107
|
],
|
|
107
108
|
}, { format: flags.format, jq: flags.jq });
|
|
@@ -351,15 +352,30 @@ async function cmdAuth(args, flags, env) {
|
|
|
351
352
|
});
|
|
352
353
|
if (sub === "status") {
|
|
353
354
|
const verify = args.includes("--verify");
|
|
354
|
-
|
|
355
|
+
let tokens = (0, store_1.loadTokens)(profile.name, env);
|
|
355
356
|
if (!tokens) {
|
|
356
357
|
(0, envelope_1.writeSuccess)({
|
|
357
358
|
authenticated: false,
|
|
359
|
+
session: "none",
|
|
358
360
|
profile: profile.name,
|
|
359
361
|
verified: false,
|
|
362
|
+
refresh: "no_session",
|
|
363
|
+
accessTokenExpired: null,
|
|
364
|
+
hasRefreshToken: false,
|
|
360
365
|
}, { format: flags.format, jq: flags.jq });
|
|
361
366
|
return 0;
|
|
362
367
|
}
|
|
368
|
+
let refresh = "skipped";
|
|
369
|
+
if ((0, refresh_1.accessTokenNeedsRefresh)(tokens)) {
|
|
370
|
+
const outcome = await (0, refresh_1.refreshStoredTokens)(profile, env);
|
|
371
|
+
refresh = outcome.status;
|
|
372
|
+
if (outcome.status === "refreshed" || outcome.status === "unchanged") {
|
|
373
|
+
tokens = outcome.tokens;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
tokens = (0, store_1.loadTokens)(profile.name, env) ?? tokens;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
363
379
|
let verified = null;
|
|
364
380
|
let whoami = null;
|
|
365
381
|
if (verify) {
|
|
@@ -370,9 +386,18 @@ async function cmdAuth(args, flags, env) {
|
|
|
370
386
|
}, env);
|
|
371
387
|
verified = res.status >= 200 && res.status < 300;
|
|
372
388
|
whoami = verified ? res.json : { status: res.status, body: res.json };
|
|
389
|
+
tokens = (0, store_1.loadTokens)(profile.name, env) ?? tokens;
|
|
373
390
|
}
|
|
391
|
+
const accessTokenExpired = tokens.expiresAt <= Date.now();
|
|
392
|
+
const hasRefreshToken = Boolean(tokens.refreshToken?.trim());
|
|
393
|
+
const session = !accessTokenExpired
|
|
394
|
+
? "active"
|
|
395
|
+
: refresh === "failed"
|
|
396
|
+
? "refresh_failed"
|
|
397
|
+
: "expired";
|
|
374
398
|
(0, envelope_1.writeSuccess)({
|
|
375
|
-
authenticated:
|
|
399
|
+
authenticated: session === "active",
|
|
400
|
+
session,
|
|
376
401
|
profile: profile.name,
|
|
377
402
|
environment: tokens.environment,
|
|
378
403
|
issuer: tokens.issuer,
|
|
@@ -381,6 +406,9 @@ async function cmdAuth(args, flags, env) {
|
|
|
381
406
|
scopes: tokens.scopes,
|
|
382
407
|
accessTokenFingerprint: (0, store_1.tokenFingerprint)(tokens.accessToken),
|
|
383
408
|
expiresAt: tokens.expiresAt,
|
|
409
|
+
accessTokenExpired,
|
|
410
|
+
hasRefreshToken,
|
|
411
|
+
refresh,
|
|
384
412
|
verified,
|
|
385
413
|
whoami,
|
|
386
414
|
}, { format: flags.format, jq: flags.jq });
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import type { SymbolMetadata } from "./types";
|
|
1
2
|
export declare const MARKET_SYMBOLS_PATH = "/api/v1/market/symbols";
|
|
2
3
|
export declare function marketSymbolsPath(exchangeId: string): string;
|
|
4
|
+
export interface MarketCatalogPayload {
|
|
5
|
+
readonly symbols: string[];
|
|
6
|
+
readonly symbolMetadata: Readonly<Record<string, SymbolMetadata>>;
|
|
7
|
+
}
|
|
3
8
|
export declare function extractCatalogSymbols(json: unknown): string[];
|
|
9
|
+
export declare function extractMarketCatalog(json: unknown): MarketCatalogPayload;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MARKET_SYMBOLS_PATH = void 0;
|
|
4
4
|
exports.marketSymbolsPath = marketSymbolsPath;
|
|
5
5
|
exports.extractCatalogSymbols = extractCatalogSymbols;
|
|
6
|
+
exports.extractMarketCatalog = extractMarketCatalog;
|
|
6
7
|
const errors_1 = require("./errors");
|
|
7
8
|
exports.MARKET_SYMBOLS_PATH = "/api/v1/market/symbols";
|
|
8
9
|
function marketSymbolsPath(exchangeId) {
|
|
@@ -10,6 +11,9 @@ function marketSymbolsPath(exchangeId) {
|
|
|
10
11
|
return `${exports.MARKET_SYMBOLS_PATH}?${query}`;
|
|
11
12
|
}
|
|
12
13
|
function extractCatalogSymbols(json) {
|
|
14
|
+
return extractMarketCatalog(json).symbols;
|
|
15
|
+
}
|
|
16
|
+
function extractMarketCatalog(json) {
|
|
13
17
|
const root = asRecord(json);
|
|
14
18
|
const payload = asRecord(root?.data) ?? root;
|
|
15
19
|
const symbols = payload?.symbols;
|
|
@@ -40,8 +44,47 @@ function extractCatalogSymbols(json) {
|
|
|
40
44
|
details: json,
|
|
41
45
|
});
|
|
42
46
|
}
|
|
47
|
+
return {
|
|
48
|
+
symbols: out,
|
|
49
|
+
symbolMetadata: extractSymbolMetadata(payload?.symbolMetadata),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function extractSymbolMetadata(value) {
|
|
53
|
+
const rec = asRecord(value);
|
|
54
|
+
if (!rec)
|
|
55
|
+
return {};
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [rawKey, rawMeta] of Object.entries(rec)) {
|
|
58
|
+
const key = rawKey.trim();
|
|
59
|
+
const meta = parseSymbolMetadata(rawMeta);
|
|
60
|
+
if (!key || !meta)
|
|
61
|
+
continue;
|
|
62
|
+
out[key] = meta;
|
|
63
|
+
}
|
|
43
64
|
return out;
|
|
44
65
|
}
|
|
66
|
+
function parseSymbolMetadata(value) {
|
|
67
|
+
const rec = asRecord(value);
|
|
68
|
+
if (!rec)
|
|
69
|
+
return undefined;
|
|
70
|
+
const meta = {};
|
|
71
|
+
if (typeof rec.isTradFiRwa === "boolean") {
|
|
72
|
+
meta.isTradFiRwa = rec.isTradFiRwa;
|
|
73
|
+
}
|
|
74
|
+
if (typeof rec.assetClass === "string" && rec.assetClass.trim()) {
|
|
75
|
+
meta.assetClass = rec.assetClass.trim();
|
|
76
|
+
}
|
|
77
|
+
if (isFiniteNumber(rec.minAmount))
|
|
78
|
+
meta.minAmount = rec.minAmount;
|
|
79
|
+
if (isFiniteNumber(rec.minCost))
|
|
80
|
+
meta.minCost = rec.minCost;
|
|
81
|
+
if (isFiniteNumber(rec.contractSize))
|
|
82
|
+
meta.contractSize = rec.contractSize;
|
|
83
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
84
|
+
}
|
|
85
|
+
function isFiniteNumber(value) {
|
|
86
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
87
|
+
}
|
|
45
88
|
function asRecord(value) {
|
|
46
89
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
47
90
|
return undefined;
|
|
@@ -42,12 +42,15 @@ for (const exchange of exports.RESOLVE_SYMBOLS_EXCHANGES) {
|
|
|
42
42
|
EXCHANGE_BY_ALIAS.set(alias.toLowerCase(), exchange);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
const PASSTHROUGH_EXCHANGE_ID = /^[a-z][a-z0-9_]{1,63}$/;
|
|
45
46
|
function resolveSymbolsExchangeId(raw) {
|
|
46
47
|
const key = raw.trim().toLowerCase();
|
|
47
48
|
const exchange = EXCHANGE_BY_ALIAS.get(key);
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if (exchange)
|
|
50
|
+
return exchange;
|
|
51
|
+
if (PASSTHROUGH_EXCHANGE_ID.test(key)) {
|
|
52
|
+
return { id: key, label: raw.trim(), aliases: [key] };
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
const allowed = exports.RESOLVE_SYMBOLS_EXCHANGES.map((item) => item.aliases[0]).join("|");
|
|
55
|
+
throw new Error(`--exchange must be ${allowed} or a market.symbols.list catalog id (got ${raw.trim() || "<empty>"})`);
|
|
53
56
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { CatalogSymbol, ResolveSymbolsQueryResult } from "./types";
|
|
1
|
+
import type { CatalogSymbol, ResolveAssetClassFilter, ResolveSymbolsQueryResult, SymbolMetadata } from "./types";
|
|
2
2
|
export declare function normalizeSymbolSearchKey(value: string): string;
|
|
3
3
|
export declare function parseCatalogSymbol(symbol: string): CatalogSymbol | null;
|
|
4
|
-
export declare function indexCatalogSymbols(symbols: readonly string[]): readonly CatalogSymbol[];
|
|
5
|
-
export declare function
|
|
4
|
+
export declare function indexCatalogSymbols(symbols: readonly string[], metadata?: Readonly<Record<string, SymbolMetadata>>): readonly CatalogSymbol[];
|
|
5
|
+
export declare function catalogMatchesAssetClass(item: CatalogSymbol, assetClass: ResolveAssetClassFilter): boolean;
|
|
6
|
+
export declare function resolveQueryAgainstCatalog(query: string, catalog: readonly CatalogSymbol[], limit?: number, assetClass?: ResolveAssetClassFilter): ResolveSymbolsQueryResult;
|
|
6
7
|
export declare function levenshtein(left: string, right: string): number;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.normalizeSymbolSearchKey = normalizeSymbolSearchKey;
|
|
4
4
|
exports.parseCatalogSymbol = parseCatalogSymbol;
|
|
5
5
|
exports.indexCatalogSymbols = indexCatalogSymbols;
|
|
6
|
+
exports.catalogMatchesAssetClass = catalogMatchesAssetClass;
|
|
6
7
|
exports.resolveQueryAgainstCatalog = resolveQueryAgainstCatalog;
|
|
7
8
|
exports.levenshtein = levenshtein;
|
|
8
9
|
const LINEAR_PERP_PATTERN = /^([\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*)\/(USDT|USDC|USD):\2$/u;
|
|
@@ -69,7 +70,7 @@ function parseCatalogSymbol(symbol) {
|
|
|
69
70
|
compactKey: normalizeSymbolSearchKey(upper),
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
|
-
function indexCatalogSymbols(symbols) {
|
|
73
|
+
function indexCatalogSymbols(symbols, metadata = {}) {
|
|
73
74
|
const seen = new Set();
|
|
74
75
|
const indexed = [];
|
|
75
76
|
for (const raw of symbols) {
|
|
@@ -79,64 +80,59 @@ function indexCatalogSymbols(symbols) {
|
|
|
79
80
|
if (seen.has(parsed.symbol))
|
|
80
81
|
continue;
|
|
81
82
|
seen.add(parsed.symbol);
|
|
82
|
-
indexed.push(
|
|
83
|
+
indexed.push({
|
|
84
|
+
...parsed,
|
|
85
|
+
metadata: metadata[raw] ?? metadata[parsed.symbol],
|
|
86
|
+
});
|
|
83
87
|
}
|
|
84
88
|
return indexed;
|
|
85
89
|
}
|
|
86
|
-
function
|
|
90
|
+
function catalogMatchesAssetClass(item, assetClass) {
|
|
91
|
+
if (assetClass === "all")
|
|
92
|
+
return true;
|
|
93
|
+
const cls = item.metadata?.assetClass;
|
|
94
|
+
const tradfi = item.metadata?.isTradFiRwa === true;
|
|
95
|
+
if (assetClass === "equity_perp")
|
|
96
|
+
return cls === "equity_perp";
|
|
97
|
+
if (assetClass === "rwa_perp")
|
|
98
|
+
return cls === "rwa_perp";
|
|
99
|
+
return !tradfi && cls !== "equity_perp" && cls !== "rwa_perp";
|
|
100
|
+
}
|
|
101
|
+
function resolveQueryAgainstCatalog(query, catalog, limit = 8, assetClass = "all") {
|
|
87
102
|
const trimmed = query.trim();
|
|
88
|
-
const
|
|
103
|
+
const scoped = catalog.filter((item) => catalogMatchesAssetClass(item, assetClass));
|
|
104
|
+
const matches = rankMatches(trimmed, scoped);
|
|
89
105
|
const exact = matches.filter((item) => EXACT_REASONS.has(item.reason));
|
|
90
106
|
const close = matches.filter((item) => !EXACT_REASONS.has(item.reason));
|
|
91
107
|
const capped = (items) => items.slice(0, Math.max(1, limit));
|
|
92
108
|
if (exact.length === 1) {
|
|
93
|
-
return
|
|
94
|
-
query: trimmed,
|
|
95
|
-
status: "exact",
|
|
96
|
-
resolved: exact[0].symbol,
|
|
97
|
-
needsConfirmation: false,
|
|
98
|
-
matches: exact,
|
|
99
|
-
matchCount: exact.length,
|
|
100
|
-
};
|
|
109
|
+
return result("exact", exact[0].symbol, false, exact, exact.length);
|
|
101
110
|
}
|
|
102
111
|
if (exact.length > 1) {
|
|
103
|
-
return
|
|
104
|
-
query: trimmed,
|
|
105
|
-
status: "ambiguous",
|
|
106
|
-
resolved: null,
|
|
107
|
-
needsConfirmation: true,
|
|
108
|
-
matches: capped(exact),
|
|
109
|
-
matchCount: exact.length,
|
|
110
|
-
};
|
|
112
|
+
return result("ambiguous", null, true, capped(exact), exact.length);
|
|
111
113
|
}
|
|
112
114
|
if (close.length === 1) {
|
|
113
|
-
return
|
|
114
|
-
query: trimmed,
|
|
115
|
-
status: "close",
|
|
116
|
-
resolved: close[0].symbol,
|
|
117
|
-
needsConfirmation: true,
|
|
118
|
-
matches: close,
|
|
119
|
-
matchCount: 1,
|
|
120
|
-
};
|
|
115
|
+
return result("close", close[0].symbol, true, close, 1);
|
|
121
116
|
}
|
|
122
117
|
if (close.length > 1) {
|
|
118
|
+
return result("ambiguous", null, true, capped(close), close.length);
|
|
119
|
+
}
|
|
120
|
+
return result("none", null, false, [], 0);
|
|
121
|
+
function result(status, resolved, needsConfirmation, matches, matchCount) {
|
|
122
|
+
const chosen = resolved
|
|
123
|
+
? matches.find((item) => item.symbol === resolved)
|
|
124
|
+
: undefined;
|
|
123
125
|
return {
|
|
124
126
|
query: trimmed,
|
|
125
|
-
status
|
|
126
|
-
resolved
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
status,
|
|
128
|
+
resolved,
|
|
129
|
+
assetClass: chosen?.assetClass ?? null,
|
|
130
|
+
isTradFiRwa: chosen?.isTradFiRwa ?? false,
|
|
131
|
+
needsConfirmation,
|
|
132
|
+
matches,
|
|
133
|
+
matchCount,
|
|
130
134
|
};
|
|
131
135
|
}
|
|
132
|
-
return {
|
|
133
|
-
query: trimmed,
|
|
134
|
-
status: "none",
|
|
135
|
-
resolved: null,
|
|
136
|
-
needsConfirmation: false,
|
|
137
|
-
matches: [],
|
|
138
|
-
matchCount: 0,
|
|
139
|
-
};
|
|
140
136
|
}
|
|
141
137
|
function rankMatches(query, catalog) {
|
|
142
138
|
const canonicalQuery = query.trim().toUpperCase();
|
|
@@ -161,21 +157,21 @@ function rankMatches(query, catalog) {
|
|
|
161
157
|
}
|
|
162
158
|
function rankCatalogSymbol(item, canonicalQuery, queryKey, queryBaseKey) {
|
|
163
159
|
if (item.symbol === canonicalQuery) {
|
|
164
|
-
return match(item
|
|
160
|
+
return match(item, "exact_canonical", 100);
|
|
165
161
|
}
|
|
166
162
|
if (item.pair === canonicalQuery) {
|
|
167
|
-
return match(item
|
|
163
|
+
return match(item, "exact_pair", 96);
|
|
168
164
|
}
|
|
169
165
|
if (item.compactKey === queryKey || item.searchKey === queryKey) {
|
|
170
|
-
return match(item
|
|
166
|
+
return match(item, "exact_compact", 98);
|
|
171
167
|
}
|
|
172
168
|
if (item.baseKey === queryKey || item.baseKey === queryBaseKey) {
|
|
173
|
-
return match(item
|
|
169
|
+
return match(item, "exact_base", 95);
|
|
174
170
|
}
|
|
175
171
|
let best = null;
|
|
176
172
|
const consider = (reason, score) => {
|
|
177
173
|
if (!best || score > best.score) {
|
|
178
|
-
best = match(item
|
|
174
|
+
best = match(item, reason, score);
|
|
179
175
|
}
|
|
180
176
|
};
|
|
181
177
|
if (queryBaseKey.length >= 2 && item.baseKey.startsWith(queryBaseKey)) {
|
|
@@ -219,8 +215,14 @@ function isCloseDistance(left, right, distance) {
|
|
|
219
215
|
return distance === 1;
|
|
220
216
|
return distance <= 2;
|
|
221
217
|
}
|
|
222
|
-
function match(
|
|
223
|
-
return {
|
|
218
|
+
function match(item, reason, score) {
|
|
219
|
+
return {
|
|
220
|
+
symbol: item.symbol,
|
|
221
|
+
reason,
|
|
222
|
+
score,
|
|
223
|
+
assetClass: item.metadata?.assetClass ?? null,
|
|
224
|
+
isTradFiRwa: item.metadata?.isTradFiRwa === true,
|
|
225
|
+
};
|
|
224
226
|
}
|
|
225
227
|
function clampScore(score) {
|
|
226
228
|
return Math.max(1, Math.min(94, score));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolveSymbolsRunArgs } from "./types";
|
|
2
2
|
export declare const RESOLVE_SYMBOLS_DEFAULT_LIMIT = 8;
|
|
3
3
|
export declare const RESOLVE_SYMBOLS_MAX_LIMIT = 25;
|
|
4
|
+
export declare const RESOLVE_SYMBOLS_ASSET_CLASSES: readonly ["all", "equity_perp", "rwa_perp", "crypto"];
|
|
4
5
|
export declare const RESOLVE_SYMBOLS_USAGE: string[];
|
|
5
6
|
export declare function parseResolveSymbolsArgs(args: readonly string[]): ResolveSymbolsRunArgs;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RESOLVE_SYMBOLS_USAGE = exports.RESOLVE_SYMBOLS_MAX_LIMIT = exports.RESOLVE_SYMBOLS_DEFAULT_LIMIT = void 0;
|
|
3
|
+
exports.RESOLVE_SYMBOLS_USAGE = exports.RESOLVE_SYMBOLS_ASSET_CLASSES = exports.RESOLVE_SYMBOLS_MAX_LIMIT = exports.RESOLVE_SYMBOLS_DEFAULT_LIMIT = void 0;
|
|
4
4
|
exports.parseResolveSymbolsArgs = parseResolveSymbolsArgs;
|
|
5
5
|
const errors_1 = require("./errors");
|
|
6
6
|
const exchanges_1 = require("./exchanges");
|
|
7
7
|
exports.RESOLVE_SYMBOLS_DEFAULT_LIMIT = 8;
|
|
8
8
|
exports.RESOLVE_SYMBOLS_MAX_LIMIT = 25;
|
|
9
|
+
exports.RESOLVE_SYMBOLS_ASSET_CLASSES = [
|
|
10
|
+
"all",
|
|
11
|
+
"equity_perp",
|
|
12
|
+
"rwa_perp",
|
|
13
|
+
"crypto",
|
|
14
|
+
];
|
|
9
15
|
exports.RESOLVE_SYMBOLS_USAGE = [
|
|
10
|
-
"alphafox resolve-symbols <query...> [--exchange binance] [--limit 8]",
|
|
16
|
+
"alphafox resolve-symbols <query...> [--exchange binance] [--asset-class all] [--limit 8]",
|
|
11
17
|
"alphafox resolve-symbols --query BTC --query ETH --exchange okx",
|
|
18
|
+
"alphafox resolve-symbols NVDA AAPL TSLA --exchange binance --asset-class equity_perp",
|
|
12
19
|
];
|
|
13
20
|
function usage(message, subtype = "invalid_args") {
|
|
14
21
|
throw new errors_1.ResolveSymbolsError({
|
|
@@ -30,6 +37,7 @@ function parseResolveSymbolsArgs(args) {
|
|
|
30
37
|
const queries = [];
|
|
31
38
|
let exchange = exchanges_1.RESOLVE_SYMBOLS_DEFAULT_EXCHANGE;
|
|
32
39
|
let limit = exports.RESOLVE_SYMBOLS_DEFAULT_LIMIT;
|
|
40
|
+
let assetClass = "all";
|
|
33
41
|
let help = false;
|
|
34
42
|
for (let i = 0; i < args.length; i += 1) {
|
|
35
43
|
const a = args[i];
|
|
@@ -59,6 +67,14 @@ function parseResolveSymbolsArgs(args) {
|
|
|
59
67
|
case "--exchange":
|
|
60
68
|
exchange = read();
|
|
61
69
|
break;
|
|
70
|
+
case "--asset-class": {
|
|
71
|
+
const value = read().trim().toLowerCase();
|
|
72
|
+
if (!exports.RESOLVE_SYMBOLS_ASSET_CLASSES.includes(value)) {
|
|
73
|
+
usage(`--asset-class must be ${exports.RESOLVE_SYMBOLS_ASSET_CLASSES.join("|")}`, "invalid_asset_class");
|
|
74
|
+
}
|
|
75
|
+
assetClass = value;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
62
78
|
case "--limit": {
|
|
63
79
|
const n = Number(read());
|
|
64
80
|
if (!Number.isInteger(n) || n <= 0 || n > exports.RESOLVE_SYMBOLS_MAX_LIMIT) {
|
|
@@ -72,7 +88,7 @@ function parseResolveSymbolsArgs(args) {
|
|
|
72
88
|
}
|
|
73
89
|
}
|
|
74
90
|
if (help) {
|
|
75
|
-
return { help: true, queries: [], exchange, limit };
|
|
91
|
+
return { help: true, queries: [], exchange, limit, assetClass };
|
|
76
92
|
}
|
|
77
93
|
const normalizedQueries = queries.map((item) => item.trim()).filter(Boolean);
|
|
78
94
|
if (normalizedQueries.length === 0) {
|
|
@@ -90,6 +106,7 @@ function parseResolveSymbolsArgs(args) {
|
|
|
90
106
|
queries: uniqueQueries(normalizedQueries),
|
|
91
107
|
exchange: resolvedExchange,
|
|
92
108
|
limit,
|
|
109
|
+
assetClass,
|
|
93
110
|
};
|
|
94
111
|
}
|
|
95
112
|
function uniqueQueries(queries) {
|
|
@@ -41,10 +41,12 @@ function resolveSymbolsHelpData() {
|
|
|
41
41
|
name: "resolve-symbols",
|
|
42
42
|
usage: parse_args_1.RESOLVE_SYMBOLS_USAGE,
|
|
43
43
|
notes: [
|
|
44
|
-
"Resolves user-mentioned tickers against
|
|
45
|
-
"Default --exchange is binance (binance_perp_usdt).
|
|
46
|
-
"
|
|
47
|
-
"
|
|
44
|
+
"Resolves user-mentioned tickers against market.symbols.list for the chosen catalog",
|
|
45
|
+
"Default --exchange is binance (binance_perp_usdt). Built-in aliases: binance|okx|bybit|bitget|hyperliquid|aster",
|
|
46
|
+
"US stock perps live in the same catalog (NVDA/USDT:USDT) and are tagged symbolMetadata.assetClass=equity_perp / isTradFiRwa",
|
|
47
|
+
"For 美股 use --asset-class equity_perp. For gold/silver RWAs use rwa_perp. Crypto perps are untagged or --asset-class crypto",
|
|
48
|
+
"Each match includes assetClass and isTradFiRwa. Do not treat an equity_perp as a crypto coin",
|
|
49
|
+
"Catalog dump remains alphafox market symbols list --exchange <id>",
|
|
48
50
|
],
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -77,12 +79,22 @@ async function executeResolveSymbols(args, flags, env = process.env, deps = {})
|
|
|
77
79
|
details: res.json,
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
|
-
const
|
|
82
|
+
const payload = (0, catalog_1.extractMarketCatalog)(res.json);
|
|
83
|
+
const catalog = (0, match_1.indexCatalogSymbols)(payload.symbols, payload.symbolMetadata);
|
|
84
|
+
if (args.assetClass !== "all" &&
|
|
85
|
+
!catalog.some((item) => item.metadata?.assetClass)) {
|
|
86
|
+
throw new errors_1.ResolveSymbolsError({
|
|
87
|
+
type: "runtime",
|
|
88
|
+
subtype: "metadata_missing",
|
|
89
|
+
message: "market.symbols.list did not include symbolMetadata; cannot apply --asset-class",
|
|
90
|
+
hint: "Retry without --asset-class, or use a catalog that returns symbolMetadata.assetClass.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
81
93
|
return {
|
|
82
94
|
exchange: exchange.id,
|
|
83
95
|
exchangeLabel: exchange.label,
|
|
84
96
|
catalogSize: catalog.length,
|
|
85
|
-
queries: args.queries.map((query) => (0, match_1.resolveQueryAgainstCatalog)(query, catalog, args.limit)),
|
|
97
|
+
queries: args.queries.map((query) => (0, match_1.resolveQueryAgainstCatalog)(query, catalog, args.limit, args.assetClass)),
|
|
86
98
|
};
|
|
87
99
|
}
|
|
88
100
|
async function cmdResolveSymbols(args, flags, env = process.env, deps = {}) {
|
|
@@ -119,6 +131,7 @@ async function cmdResolveSymbols(args, flags, env = process.env, deps = {}) {
|
|
|
119
131
|
path: (0, catalog_1.marketSymbolsPath)(exchange.id),
|
|
120
132
|
exchange: exchange.id,
|
|
121
133
|
exchangeLabel: exchange.label,
|
|
134
|
+
assetClass: parsed.assetClass,
|
|
122
135
|
queries: parsed.queries,
|
|
123
136
|
}, { format: flags.format, jq: flags.jq });
|
|
124
137
|
return 0;
|