@alphafox/cli 0.3.2 → 0.3.4
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 +4 -3
- package/dist/auth/loopback-callback.js +6 -6
- package/dist/catalog/allowlist.d.ts +1 -1
- package/dist/catalog/allowlist.js +1 -1
- package/dist/catalog/generated/registry.json +338 -49
- package/dist/catalog/generated/schemas.json +1880 -779
- package/dist/catalog/omit.d.ts +6 -0
- package/dist/catalog/omit.js +11 -0
- package/dist/catalog/operations.d.ts +1 -1
- package/dist/catalog/operations.js +5 -2
- package/dist/commands/run.js +10 -3
- package/dist/engine-backtest/load-config.d.ts +4 -0
- package/dist/engine-backtest/load-config.js +44 -0
- package/dist/engine-backtest/parse-args.d.ts +3 -1
- package/dist/engine-backtest/parse-args.js +87 -3
- package/dist/engine-backtest/parse-axes.d.ts +3 -0
- package/dist/engine-backtest/parse-axes.js +167 -0
- package/dist/engine-backtest/persist.d.ts +60 -1
- package/dist/engine-backtest/persist.js +196 -1
- package/dist/engine-backtest/return-curve.d.ts +27 -0
- package/dist/engine-backtest/return-curve.js +80 -0
- package/dist/engine-backtest/run-command.d.ts +1 -4
- package/dist/engine-backtest/run-command.js +61 -45
- package/dist/engine-backtest/sweep-command.d.ts +13 -0
- package/dist/engine-backtest/sweep-command.js +749 -0
- package/dist/engine-backtest/sweep-kernel/caps.d.ts +26 -0
- package/dist/engine-backtest/sweep-kernel/caps.js +48 -0
- package/dist/engine-backtest/sweep-kernel/index.d.ts +13 -0
- package/dist/engine-backtest/sweep-kernel/index.js +34 -0
- package/dist/engine-backtest/sweep-kernel/plan.d.ts +22 -0
- package/dist/engine-backtest/sweep-kernel/plan.js +320 -0
- package/dist/engine-backtest/sweep-kernel/results.d.ts +72 -0
- package/dist/engine-backtest/sweep-kernel/results.js +150 -0
- package/dist/engine-backtest/sweep-kernel/session.d.ts +55 -0
- package/dist/engine-backtest/sweep-kernel/session.js +56 -0
- package/dist/engine-backtest/sweep-kernel/types.d.ts +36 -0
- package/dist/engine-backtest/sweep-kernel/types.js +2 -0
- package/dist/engine-backtest/types.d.ts +130 -0
- package/dist/install/exec.js +5 -1
- package/dist/install/wizard.js +26 -29
- package/dist/resolve-symbols/catalog.d.ts +3 -0
- package/dist/resolve-symbols/catalog.js +50 -0
- package/dist/resolve-symbols/errors.d.ts +18 -0
- package/dist/resolve-symbols/errors.js +26 -0
- package/dist/resolve-symbols/exchanges.d.ts +8 -0
- package/dist/resolve-symbols/exchanges.js +53 -0
- package/dist/resolve-symbols/match.d.ts +6 -0
- package/dist/resolve-symbols/match.js +250 -0
- package/dist/resolve-symbols/parse-args.d.ts +5 -0
- package/dist/resolve-symbols/parse-args.js +106 -0
- package/dist/resolve-symbols/run-command.d.ts +21 -0
- package/dist/resolve-symbols/run-command.js +149 -0
- package/dist/resolve-symbols/types.d.ts +37 -0
- package/dist/resolve-symbols/types.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/alphafox-cli-installation-guide.md +2 -2
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +38 -0
- package/skills/alphafox-shared/SKILL.md +5 -1
- package/skills/auth/SKILL.md +1 -1
- package/skills/engine-backtest/SKILL.md +48 -6
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +27 -4
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +7 -20
- package/skills/trading/SKILL.md +3 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SweepSubscriptionTier } from "./types";
|
|
2
|
+
/** Free tier max simultaneous sweep parameters. */
|
|
3
|
+
export declare const FREE_MAX_SWEEP_PARAMS = 5;
|
|
4
|
+
/** Free combination cap (downsample above this). */
|
|
5
|
+
export declare const FREE_MAX_SWEEP_COMBINATIONS = 125;
|
|
6
|
+
/** Pro / Pro Max combination cap. */
|
|
7
|
+
export declare const PRO_MAX_SWEEP_COMBINATIONS = 2000;
|
|
8
|
+
/** Free tier is forced to serial execution. */
|
|
9
|
+
export declare const SWEEP_SERIAL_CONCURRENCY = 1;
|
|
10
|
+
/** Pro default worker count for parameter search. */
|
|
11
|
+
export declare const DEFAULT_SWEEP_CONCURRENCY = 2;
|
|
12
|
+
/** Hard cap on simultaneous sweep workers. */
|
|
13
|
+
export declare const MAX_SWEEP_CONCURRENCY = 8;
|
|
14
|
+
export declare const SWEEP_CONCURRENCY_OPTIONS: readonly [1, 2, 3, 4, 5, 6, 7, 8];
|
|
15
|
+
export type SweepConcurrency = (typeof SWEEP_CONCURRENCY_OPTIONS)[number];
|
|
16
|
+
/**
|
|
17
|
+
* Free users always run serially. Pro / Pro Max may pick 1…MAX, defaulting to
|
|
18
|
+
* {@link DEFAULT_SWEEP_CONCURRENCY} when the requested value is invalid.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSweepConcurrency(input: {
|
|
21
|
+
readonly subscriptionTier: SweepSubscriptionTier | undefined;
|
|
22
|
+
readonly requested: number;
|
|
23
|
+
}): SweepConcurrency;
|
|
24
|
+
/** null = unlimited param count (Pro / Pro Max). */
|
|
25
|
+
export declare function resolveMaxSweepParams(subscriptionTier: SweepSubscriptionTier | undefined): number | null;
|
|
26
|
+
export declare function resolveMaxSweepCombinations(subscriptionTier: SweepSubscriptionTier | undefined): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SWEEP_CONCURRENCY_OPTIONS = exports.MAX_SWEEP_CONCURRENCY = exports.DEFAULT_SWEEP_CONCURRENCY = exports.SWEEP_SERIAL_CONCURRENCY = exports.PRO_MAX_SWEEP_COMBINATIONS = exports.FREE_MAX_SWEEP_COMBINATIONS = exports.FREE_MAX_SWEEP_PARAMS = void 0;
|
|
4
|
+
exports.resolveSweepConcurrency = resolveSweepConcurrency;
|
|
5
|
+
exports.resolveMaxSweepParams = resolveMaxSweepParams;
|
|
6
|
+
exports.resolveMaxSweepCombinations = resolveMaxSweepCombinations;
|
|
7
|
+
/** Free tier max simultaneous sweep parameters. */
|
|
8
|
+
exports.FREE_MAX_SWEEP_PARAMS = 5;
|
|
9
|
+
/** Free combination cap (downsample above this). */
|
|
10
|
+
exports.FREE_MAX_SWEEP_COMBINATIONS = 125;
|
|
11
|
+
/** Pro / Pro Max combination cap. */
|
|
12
|
+
exports.PRO_MAX_SWEEP_COMBINATIONS = 2000;
|
|
13
|
+
/** Free tier is forced to serial execution. */
|
|
14
|
+
exports.SWEEP_SERIAL_CONCURRENCY = 1;
|
|
15
|
+
/** Pro default worker count for parameter search. */
|
|
16
|
+
exports.DEFAULT_SWEEP_CONCURRENCY = 2;
|
|
17
|
+
/** Hard cap on simultaneous sweep workers. */
|
|
18
|
+
exports.MAX_SWEEP_CONCURRENCY = 8;
|
|
19
|
+
exports.SWEEP_CONCURRENCY_OPTIONS = [
|
|
20
|
+
1, 2, 3, 4, 5, 6, 7, 8,
|
|
21
|
+
];
|
|
22
|
+
function isProTier(subscriptionTier) {
|
|
23
|
+
return subscriptionTier === "pro" || subscriptionTier === "pro_max";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Free users always run serially. Pro / Pro Max may pick 1…MAX, defaulting to
|
|
27
|
+
* {@link DEFAULT_SWEEP_CONCURRENCY} when the requested value is invalid.
|
|
28
|
+
*/
|
|
29
|
+
function resolveSweepConcurrency(input) {
|
|
30
|
+
if (!isProTier(input.subscriptionTier)) {
|
|
31
|
+
return exports.SWEEP_SERIAL_CONCURRENCY;
|
|
32
|
+
}
|
|
33
|
+
if (Number.isInteger(input.requested) &&
|
|
34
|
+
input.requested >= exports.SWEEP_SERIAL_CONCURRENCY &&
|
|
35
|
+
input.requested <= exports.MAX_SWEEP_CONCURRENCY) {
|
|
36
|
+
return input.requested;
|
|
37
|
+
}
|
|
38
|
+
return exports.DEFAULT_SWEEP_CONCURRENCY;
|
|
39
|
+
}
|
|
40
|
+
/** null = unlimited param count (Pro / Pro Max). */
|
|
41
|
+
function resolveMaxSweepParams(subscriptionTier) {
|
|
42
|
+
return isProTier(subscriptionTier) ? null : exports.FREE_MAX_SWEEP_PARAMS;
|
|
43
|
+
}
|
|
44
|
+
function resolveMaxSweepCombinations(subscriptionTier) {
|
|
45
|
+
return isProTier(subscriptionTier)
|
|
46
|
+
? exports.PRO_MAX_SWEEP_COMBINATIONS
|
|
47
|
+
: exports.FREE_MAX_SWEEP_COMBINATIONS;
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Sweep domain kernel shared with alphafox-web.
|
|
3
|
+
* Copied from alphafox-web/features/engine-backtest/domain/sweep-kernel.
|
|
4
|
+
* Keep this folder and the matching tests/sweep-kernel.*.test.ts aligned
|
|
5
|
+
* with the Web copy. No DOM, IndexedDB, React, or WASM host dependencies.
|
|
6
|
+
*/
|
|
7
|
+
export { DEFAULT_SWEEP_CONCURRENCY, FREE_MAX_SWEEP_COMBINATIONS, FREE_MAX_SWEEP_PARAMS, MAX_SWEEP_CONCURRENCY, PRO_MAX_SWEEP_COMBINATIONS, resolveMaxSweepCombinations, resolveMaxSweepParams, resolveSweepConcurrency, SWEEP_CONCURRENCY_OPTIONS, SWEEP_SERIAL_CONCURRENCY, type SweepConcurrency, } from "./caps";
|
|
8
|
+
export { applySweepCoordinate, buildSweepAxisValues, estimateFastSweepCombinationCount, planSweep, planSweepFastRefinement, } from "./plan";
|
|
9
|
+
export { analyzeSweepIsland, extractSweepMetrics, resolveIslandRiskLevel, selectBestNonLiquidatedPoint, } from "./results";
|
|
10
|
+
export { createSweepSessionState, reduceSweepSession, } from "./session";
|
|
11
|
+
export type { SweepCloudSaveState, SweepExecutionState, SweepSessionEvent, SweepSessionState, } from "./session";
|
|
12
|
+
export type { IslandAnalysis, IslandCounterexample, IslandRiskLevel, IslandWarning, ScoredSweepPoint, SweepMetricPoint, SweepMetricsSource, SweepPoint, SweepPointMetrics, } from "./results";
|
|
13
|
+
export type { SweepAxisInput, SweepAxisWindow, SweepConfigRecord, SweepCoordinate, SweepPlan, SweepPlanAxis, SweepSearchMode, SweepSubscriptionTier, } from "./types";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reduceSweepSession = exports.createSweepSessionState = exports.selectBestNonLiquidatedPoint = exports.resolveIslandRiskLevel = exports.extractSweepMetrics = exports.analyzeSweepIsland = exports.planSweepFastRefinement = exports.planSweep = exports.estimateFastSweepCombinationCount = exports.buildSweepAxisValues = exports.applySweepCoordinate = exports.SWEEP_SERIAL_CONCURRENCY = exports.SWEEP_CONCURRENCY_OPTIONS = exports.resolveSweepConcurrency = exports.resolveMaxSweepParams = exports.resolveMaxSweepCombinations = exports.PRO_MAX_SWEEP_COMBINATIONS = exports.MAX_SWEEP_CONCURRENCY = exports.FREE_MAX_SWEEP_PARAMS = exports.FREE_MAX_SWEEP_COMBINATIONS = exports.DEFAULT_SWEEP_CONCURRENCY = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Pure Sweep domain kernel shared with alphafox-web.
|
|
6
|
+
* Copied from alphafox-web/features/engine-backtest/domain/sweep-kernel.
|
|
7
|
+
* Keep this folder and the matching tests/sweep-kernel.*.test.ts aligned
|
|
8
|
+
* with the Web copy. No DOM, IndexedDB, React, or WASM host dependencies.
|
|
9
|
+
*/
|
|
10
|
+
var caps_1 = require("./caps");
|
|
11
|
+
Object.defineProperty(exports, "DEFAULT_SWEEP_CONCURRENCY", { enumerable: true, get: function () { return caps_1.DEFAULT_SWEEP_CONCURRENCY; } });
|
|
12
|
+
Object.defineProperty(exports, "FREE_MAX_SWEEP_COMBINATIONS", { enumerable: true, get: function () { return caps_1.FREE_MAX_SWEEP_COMBINATIONS; } });
|
|
13
|
+
Object.defineProperty(exports, "FREE_MAX_SWEEP_PARAMS", { enumerable: true, get: function () { return caps_1.FREE_MAX_SWEEP_PARAMS; } });
|
|
14
|
+
Object.defineProperty(exports, "MAX_SWEEP_CONCURRENCY", { enumerable: true, get: function () { return caps_1.MAX_SWEEP_CONCURRENCY; } });
|
|
15
|
+
Object.defineProperty(exports, "PRO_MAX_SWEEP_COMBINATIONS", { enumerable: true, get: function () { return caps_1.PRO_MAX_SWEEP_COMBINATIONS; } });
|
|
16
|
+
Object.defineProperty(exports, "resolveMaxSweepCombinations", { enumerable: true, get: function () { return caps_1.resolveMaxSweepCombinations; } });
|
|
17
|
+
Object.defineProperty(exports, "resolveMaxSweepParams", { enumerable: true, get: function () { return caps_1.resolveMaxSweepParams; } });
|
|
18
|
+
Object.defineProperty(exports, "resolveSweepConcurrency", { enumerable: true, get: function () { return caps_1.resolveSweepConcurrency; } });
|
|
19
|
+
Object.defineProperty(exports, "SWEEP_CONCURRENCY_OPTIONS", { enumerable: true, get: function () { return caps_1.SWEEP_CONCURRENCY_OPTIONS; } });
|
|
20
|
+
Object.defineProperty(exports, "SWEEP_SERIAL_CONCURRENCY", { enumerable: true, get: function () { return caps_1.SWEEP_SERIAL_CONCURRENCY; } });
|
|
21
|
+
var plan_1 = require("./plan");
|
|
22
|
+
Object.defineProperty(exports, "applySweepCoordinate", { enumerable: true, get: function () { return plan_1.applySweepCoordinate; } });
|
|
23
|
+
Object.defineProperty(exports, "buildSweepAxisValues", { enumerable: true, get: function () { return plan_1.buildSweepAxisValues; } });
|
|
24
|
+
Object.defineProperty(exports, "estimateFastSweepCombinationCount", { enumerable: true, get: function () { return plan_1.estimateFastSweepCombinationCount; } });
|
|
25
|
+
Object.defineProperty(exports, "planSweep", { enumerable: true, get: function () { return plan_1.planSweep; } });
|
|
26
|
+
Object.defineProperty(exports, "planSweepFastRefinement", { enumerable: true, get: function () { return plan_1.planSweepFastRefinement; } });
|
|
27
|
+
var results_1 = require("./results");
|
|
28
|
+
Object.defineProperty(exports, "analyzeSweepIsland", { enumerable: true, get: function () { return results_1.analyzeSweepIsland; } });
|
|
29
|
+
Object.defineProperty(exports, "extractSweepMetrics", { enumerable: true, get: function () { return results_1.extractSweepMetrics; } });
|
|
30
|
+
Object.defineProperty(exports, "resolveIslandRiskLevel", { enumerable: true, get: function () { return results_1.resolveIslandRiskLevel; } });
|
|
31
|
+
Object.defineProperty(exports, "selectBestNonLiquidatedPoint", { enumerable: true, get: function () { return results_1.selectBestNonLiquidatedPoint; } });
|
|
32
|
+
var session_1 = require("./session");
|
|
33
|
+
Object.defineProperty(exports, "createSweepSessionState", { enumerable: true, get: function () { return session_1.createSweepSessionState; } });
|
|
34
|
+
Object.defineProperty(exports, "reduceSweepSession", { enumerable: true, get: function () { return session_1.reduceSweepSession; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SweepAxisInput, SweepConfigRecord, SweepCoordinate, SweepPlan, SweepSearchMode, SweepSubscriptionTier } from "./types";
|
|
2
|
+
export declare function buildSweepAxisValues(axis: SweepAxisInput): number[];
|
|
3
|
+
export declare function planSweep(input: {
|
|
4
|
+
readonly axes: readonly SweepAxisInput[];
|
|
5
|
+
readonly searchMode?: SweepSearchMode;
|
|
6
|
+
readonly subscriptionTier?: SweepSubscriptionTier;
|
|
7
|
+
readonly maxCombinations?: number;
|
|
8
|
+
}): SweepPlan;
|
|
9
|
+
/**
|
|
10
|
+
* Build the second-stage local Cartesian grid around the best coarse point.
|
|
11
|
+
* Keeping a complete grid preserves the per-axis robustness slices used by the
|
|
12
|
+
* result analyzer, unlike sparse Latin-hypercube sampling.
|
|
13
|
+
*/
|
|
14
|
+
export declare function planSweepFastRefinement(input: {
|
|
15
|
+
readonly coarsePlan: SweepPlan;
|
|
16
|
+
readonly standardPlan: SweepPlan;
|
|
17
|
+
readonly center: SweepCoordinate;
|
|
18
|
+
}): SweepCoordinate[];
|
|
19
|
+
export declare function estimateFastSweepCombinationCount(coarsePlan: SweepPlan, standardPlan: SweepPlan): number;
|
|
20
|
+
export declare function applySweepCoordinate(config: SweepConfigRecord, axes: readonly {
|
|
21
|
+
readonly path: readonly string[];
|
|
22
|
+
}[], coordinate: SweepCoordinate): SweepConfigRecord;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSweepAxisValues = buildSweepAxisValues;
|
|
4
|
+
exports.planSweep = planSweep;
|
|
5
|
+
exports.planSweepFastRefinement = planSweepFastRefinement;
|
|
6
|
+
exports.estimateFastSweepCombinationCount = estimateFastSweepCombinationCount;
|
|
7
|
+
exports.applySweepCoordinate = applySweepCoordinate;
|
|
8
|
+
const caps_1 = require("./caps");
|
|
9
|
+
const MAX_RANGE_POINTS = 21;
|
|
10
|
+
const NEIGHBORHOOD_STEPS = 3;
|
|
11
|
+
const FAST_SWEEP_AXIS_POINTS = 3;
|
|
12
|
+
const EPSILON = 1e-9;
|
|
13
|
+
function buildSweepAxisValues(axis) {
|
|
14
|
+
if (axis.values) {
|
|
15
|
+
return normalizeExplicitValues(axis, axis.values);
|
|
16
|
+
}
|
|
17
|
+
const window = axis.window ?? defaultNeighborhoodWindow(axis);
|
|
18
|
+
return buildWindowValues(axis, window);
|
|
19
|
+
}
|
|
20
|
+
function planSweep(input) {
|
|
21
|
+
const searchMode = input.searchMode ?? "standard";
|
|
22
|
+
const maxParams = (0, caps_1.resolveMaxSweepParams)(input.subscriptionTier);
|
|
23
|
+
const selectedAxes = maxParams === null ? [...input.axes] : input.axes.slice(0, maxParams);
|
|
24
|
+
const rawAxes = selectedAxes.map((axis) => ({
|
|
25
|
+
path: axis.path,
|
|
26
|
+
current: axis.current,
|
|
27
|
+
values: buildSweepAxisValues(axis),
|
|
28
|
+
}));
|
|
29
|
+
if (rawAxes.length === 0 ||
|
|
30
|
+
rawAxes.some((axis) => axis.values.length === 0)) {
|
|
31
|
+
return {
|
|
32
|
+
axes: rawAxes,
|
|
33
|
+
coordinates: [],
|
|
34
|
+
requestedCombinationCount: 0,
|
|
35
|
+
sampled: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const requestedCombinationCount = product(rawAxes.map((axis) => axis.values.length));
|
|
39
|
+
const combinationCap = input.maxCombinations ??
|
|
40
|
+
(0, caps_1.resolveMaxSweepCombinations)(input.subscriptionTier);
|
|
41
|
+
const targetSizes = rawAxes.map((axis) => axis.values.length);
|
|
42
|
+
if (searchMode === "fast" && rawAxes.length > 1) {
|
|
43
|
+
for (let index = 0; index < targetSizes.length; index += 1) {
|
|
44
|
+
targetSizes[index] = Math.min(targetSizes[index] ?? FAST_SWEEP_AXIS_POINTS, FAST_SWEEP_AXIS_POINTS);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
while (product(targetSizes) > combinationCap) {
|
|
49
|
+
const axisIndex = targetSizes.reduce((largestIndex, size, index) => size > (targetSizes[largestIndex] ?? 0) ? index : largestIndex, 0);
|
|
50
|
+
targetSizes[axisIndex] = Math.max(2, (targetSizes[axisIndex] ?? 2) - 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const axes = rawAxes.map((axis, index) => ({
|
|
54
|
+
path: axis.path,
|
|
55
|
+
current: axis.current,
|
|
56
|
+
values: downsample(axis.values, targetSizes[index] ?? axis.values.length, axis.current),
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
axes,
|
|
60
|
+
coordinates: cartesianCoordinates(axes.map((axis) => axis.values)),
|
|
61
|
+
requestedCombinationCount,
|
|
62
|
+
sampled: requestedCombinationCount > product(targetSizes),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the second-stage local Cartesian grid around the best coarse point.
|
|
67
|
+
* Keeping a complete grid preserves the per-axis robustness slices used by the
|
|
68
|
+
* result analyzer, unlike sparse Latin-hypercube sampling.
|
|
69
|
+
*/
|
|
70
|
+
function planSweepFastRefinement(input) {
|
|
71
|
+
if (input.coarsePlan.axes.length <= 1 ||
|
|
72
|
+
input.coarsePlan.axes.length !== input.standardPlan.axes.length ||
|
|
73
|
+
input.center.values.length !== input.standardPlan.axes.length) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const localAxes = input.standardPlan.axes.map((axis, index) => localWindow(axis.values, input.center.values[index] ?? axis.current));
|
|
77
|
+
if (localAxes.some((values) => values.length === 0)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const coarseKeys = new Set(input.coarsePlan.coordinates.map(coordinateKey));
|
|
81
|
+
return cartesianCoordinates(localAxes).filter((coordinate) => !coarseKeys.has(coordinateKey(coordinate)));
|
|
82
|
+
}
|
|
83
|
+
function estimateFastSweepCombinationCount(coarsePlan, standardPlan) {
|
|
84
|
+
if (coarsePlan.axes.length <= 1) {
|
|
85
|
+
return coarsePlan.coordinates.length;
|
|
86
|
+
}
|
|
87
|
+
const largestRefinement = coarsePlan.coordinates.reduce((largest, center) => Math.max(largest, planSweepFastRefinement({
|
|
88
|
+
coarsePlan,
|
|
89
|
+
standardPlan,
|
|
90
|
+
center,
|
|
91
|
+
}).length), 0);
|
|
92
|
+
return coarsePlan.coordinates.length + largestRefinement;
|
|
93
|
+
}
|
|
94
|
+
function applySweepCoordinate(config, axes, coordinate) {
|
|
95
|
+
return axes.reduce((nextConfig, axis, index) => setValueAtPath(nextConfig, axis.path, coordinate.values[index] ?? getValueAtPath(nextConfig, axis.path)), config);
|
|
96
|
+
}
|
|
97
|
+
function defaultNeighborhoodWindow(axis) {
|
|
98
|
+
const step = resolveStep(axis);
|
|
99
|
+
return {
|
|
100
|
+
min: axis.current - NEIGHBORHOOD_STEPS * step,
|
|
101
|
+
max: axis.current + NEIGHBORHOOD_STEPS * step,
|
|
102
|
+
step,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function resolveStep(axis) {
|
|
106
|
+
if (axis.window && axis.window.step > 0) {
|
|
107
|
+
return axis.window.step;
|
|
108
|
+
}
|
|
109
|
+
const span = axis.min !== undefined && axis.max !== undefined && axis.max > axis.min
|
|
110
|
+
? (axis.max - axis.min) / (MAX_RANGE_POINTS - 1)
|
|
111
|
+
: Math.abs(axis.current) > 0
|
|
112
|
+
? Math.abs(axis.current) * 0.1
|
|
113
|
+
: 1;
|
|
114
|
+
if (axis.isInteger) {
|
|
115
|
+
return Math.max(1, Math.round(span));
|
|
116
|
+
}
|
|
117
|
+
const rounded = Number(span.toPrecision(2));
|
|
118
|
+
return rounded > 0 ? rounded : 0.1;
|
|
119
|
+
}
|
|
120
|
+
function normalizeExplicitValues(axis, values) {
|
|
121
|
+
return [
|
|
122
|
+
...new Set(values
|
|
123
|
+
.filter((value) => Number.isFinite(value))
|
|
124
|
+
.map((value) => roundValue(value, axis.isInteger))
|
|
125
|
+
.filter((value) => (!axis.isInteger || Number.isInteger(value)) &&
|
|
126
|
+
isWithinBounds(value, axis))),
|
|
127
|
+
].sort((left, right) => left - right);
|
|
128
|
+
}
|
|
129
|
+
function buildWindowValues(axis, window) {
|
|
130
|
+
const step = window.step;
|
|
131
|
+
if (!(step > 0) || !Number.isFinite(step)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
let min = window.min;
|
|
135
|
+
let max = window.max;
|
|
136
|
+
if (min > max) {
|
|
137
|
+
[min, max] = [max, min];
|
|
138
|
+
}
|
|
139
|
+
const raw = [];
|
|
140
|
+
const maxPoints = MAX_RANGE_POINTS * 20;
|
|
141
|
+
for (let value = min, guard = 0; value <= max + EPSILON && guard < maxPoints; value += step, guard += 1) {
|
|
142
|
+
raw.push(roundValue(value, axis.isInteger));
|
|
143
|
+
}
|
|
144
|
+
const current = roundValue(axis.current, axis.isInteger);
|
|
145
|
+
if (isWithinDraftWindow(current, min, max)) {
|
|
146
|
+
raw.push(current);
|
|
147
|
+
}
|
|
148
|
+
const unique = [
|
|
149
|
+
...new Set(raw
|
|
150
|
+
.map((value) => clamp(value, axis.min, axis.max))
|
|
151
|
+
.filter((value) => Number.isFinite(value) &&
|
|
152
|
+
(!axis.isInteger || Number.isInteger(value)) &&
|
|
153
|
+
isWithinDraftWindow(value, min, max) &&
|
|
154
|
+
isWithinBounds(value, axis))),
|
|
155
|
+
].sort((left, right) => left - right);
|
|
156
|
+
return unique.length > MAX_RANGE_POINTS
|
|
157
|
+
? downsample(unique, MAX_RANGE_POINTS, axis.current)
|
|
158
|
+
: unique;
|
|
159
|
+
}
|
|
160
|
+
function roundValue(value, isInteger) {
|
|
161
|
+
return isInteger ? Math.round(value) : Number(value.toFixed(6));
|
|
162
|
+
}
|
|
163
|
+
function clamp(value, min, max) {
|
|
164
|
+
let next = value;
|
|
165
|
+
if (min !== undefined) {
|
|
166
|
+
next = Math.max(min, next);
|
|
167
|
+
}
|
|
168
|
+
if (max !== undefined) {
|
|
169
|
+
next = Math.min(max, next);
|
|
170
|
+
}
|
|
171
|
+
return next;
|
|
172
|
+
}
|
|
173
|
+
function isWithinDraftWindow(value, min, max) {
|
|
174
|
+
return value + EPSILON >= min && value - EPSILON <= max;
|
|
175
|
+
}
|
|
176
|
+
function isWithinBounds(value, axis) {
|
|
177
|
+
if (axis.min !== undefined &&
|
|
178
|
+
(axis.minExclusive ? value <= axis.min : value < axis.min)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (axis.max !== undefined &&
|
|
182
|
+
(axis.maxExclusive ? value >= axis.max : value > axis.max)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
function downsample(values, limit, keep) {
|
|
188
|
+
if (values.length <= limit) {
|
|
189
|
+
return [...values];
|
|
190
|
+
}
|
|
191
|
+
const safeLimit = Math.max(2, Math.min(limit, values.length));
|
|
192
|
+
const selectedIndices = new Set();
|
|
193
|
+
for (let slot = 0; slot < safeLimit; slot += 1) {
|
|
194
|
+
selectedIndices.add(Math.round((slot * (values.length - 1)) / (safeLimit - 1)));
|
|
195
|
+
}
|
|
196
|
+
const keepIndex = values.indexOf(keep);
|
|
197
|
+
if (keepIndex >= 0 && !selectedIndices.has(keepIndex)) {
|
|
198
|
+
const replaceable = [...selectedIndices]
|
|
199
|
+
.filter((index) => index !== 0 && index !== values.length - 1)
|
|
200
|
+
.sort((left, right) => Math.abs(left - keepIndex) - Math.abs(right - keepIndex))[0];
|
|
201
|
+
if (replaceable !== undefined) {
|
|
202
|
+
selectedIndices.delete(replaceable);
|
|
203
|
+
}
|
|
204
|
+
selectedIndices.add(keepIndex);
|
|
205
|
+
}
|
|
206
|
+
for (let index = 0; selectedIndices.size < safeLimit; index += 1) {
|
|
207
|
+
selectedIndices.add(index);
|
|
208
|
+
}
|
|
209
|
+
return [...selectedIndices]
|
|
210
|
+
.sort((left, right) => left - right)
|
|
211
|
+
.slice(0, safeLimit)
|
|
212
|
+
.map((index) => values[index]);
|
|
213
|
+
}
|
|
214
|
+
function cartesianCoordinates(axes) {
|
|
215
|
+
return axes.reduce((coordinates, values) => coordinates.flatMap((coordinate) => values.map((value) => ({
|
|
216
|
+
values: [...coordinate.values, value],
|
|
217
|
+
}))), [{ values: [] }]);
|
|
218
|
+
}
|
|
219
|
+
function localWindow(values, center) {
|
|
220
|
+
if (values.length <= FAST_SWEEP_AXIS_POINTS) {
|
|
221
|
+
return [...values];
|
|
222
|
+
}
|
|
223
|
+
const exactIndex = values.indexOf(center);
|
|
224
|
+
const centerIndex = exactIndex >= 0
|
|
225
|
+
? exactIndex
|
|
226
|
+
: values.reduce((nearest, value, index) => Math.abs(value - center) <
|
|
227
|
+
Math.abs((values[nearest] ?? center) - center)
|
|
228
|
+
? index
|
|
229
|
+
: nearest, 0);
|
|
230
|
+
const start = Math.max(0, Math.min(centerIndex - 1, values.length - FAST_SWEEP_AXIS_POINTS));
|
|
231
|
+
return values.slice(start, start + FAST_SWEEP_AXIS_POINTS);
|
|
232
|
+
}
|
|
233
|
+
function coordinateKey(coordinate) {
|
|
234
|
+
return coordinate.values.join("\u0000");
|
|
235
|
+
}
|
|
236
|
+
function product(values) {
|
|
237
|
+
return values.reduce((total, value) => total * value, 1);
|
|
238
|
+
}
|
|
239
|
+
function isPlainRecord(value) {
|
|
240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
241
|
+
}
|
|
242
|
+
function parseArrayIndex(segment) {
|
|
243
|
+
if (!/^\d+$/.test(segment)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const index = Number(segment);
|
|
247
|
+
return Number.isInteger(index) && index >= 0 ? index : null;
|
|
248
|
+
}
|
|
249
|
+
function getValueAtPath(root, path) {
|
|
250
|
+
let current = root;
|
|
251
|
+
for (const segment of path) {
|
|
252
|
+
if (Array.isArray(current)) {
|
|
253
|
+
const index = parseArrayIndex(segment);
|
|
254
|
+
if (index === null) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
current = current[index];
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!isPlainRecord(current)) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
current = current[segment];
|
|
264
|
+
}
|
|
265
|
+
return current;
|
|
266
|
+
}
|
|
267
|
+
function containerForPathTail(existing, nextSegment) {
|
|
268
|
+
if (Array.isArray(existing) || isPlainRecord(existing)) {
|
|
269
|
+
return existing;
|
|
270
|
+
}
|
|
271
|
+
return parseArrayIndex(nextSegment ?? "") !== null ? [] : {};
|
|
272
|
+
}
|
|
273
|
+
function setValueAtPathNode(root, path, value) {
|
|
274
|
+
if (path.length === 0) {
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
const [head, ...tail] = path;
|
|
278
|
+
if (!head) {
|
|
279
|
+
throw new Error("Sweep config path segment must not be empty.");
|
|
280
|
+
}
|
|
281
|
+
if (Array.isArray(root)) {
|
|
282
|
+
const index = parseArrayIndex(head);
|
|
283
|
+
if (index === null) {
|
|
284
|
+
throw new Error(`Expected array index path segment, got "${head}".`);
|
|
285
|
+
}
|
|
286
|
+
const next = root.slice();
|
|
287
|
+
if (tail.length === 0) {
|
|
288
|
+
while (next.length <= index) {
|
|
289
|
+
next.push(null);
|
|
290
|
+
}
|
|
291
|
+
next[index] = value;
|
|
292
|
+
return next;
|
|
293
|
+
}
|
|
294
|
+
const child = containerForPathTail(next[index], tail[0]);
|
|
295
|
+
while (next.length <= index) {
|
|
296
|
+
next.push(null);
|
|
297
|
+
}
|
|
298
|
+
next[index] = setValueAtPathNode(child, tail, value);
|
|
299
|
+
return next;
|
|
300
|
+
}
|
|
301
|
+
if (!isPlainRecord(root)) {
|
|
302
|
+
return setValueAtPathNode(containerForPathTail(undefined, head), path, value);
|
|
303
|
+
}
|
|
304
|
+
const nextRoot = { ...root };
|
|
305
|
+
if (tail.length === 0) {
|
|
306
|
+
nextRoot[head] = value;
|
|
307
|
+
return nextRoot;
|
|
308
|
+
}
|
|
309
|
+
nextRoot[head] = setValueAtPathNode(containerForPathTail(nextRoot[head], tail[0]), tail, value);
|
|
310
|
+
return nextRoot;
|
|
311
|
+
}
|
|
312
|
+
function setValueAtPath(root, path, value) {
|
|
313
|
+
if (path.length === 0) {
|
|
314
|
+
if (!isPlainRecord(value)) {
|
|
315
|
+
throw new Error("Root config must be an object.");
|
|
316
|
+
}
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
return setValueAtPathNode(root, path, value);
|
|
320
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SweepCoordinate } from "./types";
|
|
2
|
+
export interface SweepPointMetrics {
|
|
3
|
+
readonly returnPct: number;
|
|
4
|
+
readonly maxDrawdownPct: number;
|
|
5
|
+
readonly sharpeRatio: number;
|
|
6
|
+
readonly winRatePct: number;
|
|
7
|
+
readonly maxLeverage?: number;
|
|
8
|
+
readonly liquidationCount: number;
|
|
9
|
+
readonly netPnl: number;
|
|
10
|
+
readonly finalEquity: number;
|
|
11
|
+
readonly tradeCount: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SweepMetricsSource {
|
|
14
|
+
readonly returnPct: number;
|
|
15
|
+
readonly maxDrawdownPct: number;
|
|
16
|
+
readonly sharpeRatio: number;
|
|
17
|
+
readonly winRatePct: number;
|
|
18
|
+
readonly maxLeverage?: number;
|
|
19
|
+
readonly liquidationCount?: number;
|
|
20
|
+
readonly liquidated?: boolean;
|
|
21
|
+
readonly netPnl: number;
|
|
22
|
+
readonly finalEquity: number;
|
|
23
|
+
readonly tradeCount: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SweepPoint {
|
|
26
|
+
readonly coordinate: SweepCoordinate;
|
|
27
|
+
readonly status: "ok" | "failed";
|
|
28
|
+
readonly metrics?: SweepPointMetrics;
|
|
29
|
+
readonly error?: string;
|
|
30
|
+
}
|
|
31
|
+
export type IslandRiskLevel = "low" | "low_medium" | "medium" | "high";
|
|
32
|
+
export interface SweepMetricPoint {
|
|
33
|
+
readonly value: number;
|
|
34
|
+
readonly returnPct: number;
|
|
35
|
+
readonly maxDrawdownPct: number;
|
|
36
|
+
readonly liquidationCount: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ScoredSweepPoint extends SweepMetricPoint {
|
|
39
|
+
readonly isCenter: boolean;
|
|
40
|
+
readonly islandScore: number;
|
|
41
|
+
readonly riskLevel: IslandRiskLevel;
|
|
42
|
+
}
|
|
43
|
+
export interface IslandCounterexample {
|
|
44
|
+
readonly counterValue: number;
|
|
45
|
+
readonly centerReturnPct: number;
|
|
46
|
+
readonly counterReturnPct: number;
|
|
47
|
+
readonly centerLiquidationCount: number;
|
|
48
|
+
readonly counterLiquidationCount: number;
|
|
49
|
+
}
|
|
50
|
+
export interface IslandWarning {
|
|
51
|
+
readonly centerValue: number;
|
|
52
|
+
readonly islandScore: number;
|
|
53
|
+
readonly riskLevel: IslandRiskLevel;
|
|
54
|
+
readonly counterexamples: readonly IslandCounterexample[];
|
|
55
|
+
}
|
|
56
|
+
export interface IslandAnalysis {
|
|
57
|
+
readonly points: readonly ScoredSweepPoint[];
|
|
58
|
+
readonly warning: IslandWarning | null;
|
|
59
|
+
readonly overallScore: number;
|
|
60
|
+
readonly overallRiskLevel: IslandRiskLevel;
|
|
61
|
+
}
|
|
62
|
+
export declare function extractSweepMetrics(source: SweepMetricsSource): SweepPointMetrics;
|
|
63
|
+
export declare function selectBestNonLiquidatedPoint(points: readonly SweepPoint[]): {
|
|
64
|
+
readonly coordinate: SweepCoordinate;
|
|
65
|
+
readonly returnPct: number;
|
|
66
|
+
} | null;
|
|
67
|
+
export declare function resolveIslandRiskLevel(score: number): IslandRiskLevel;
|
|
68
|
+
/**
|
|
69
|
+
* Score each swept point by how sharply it deviates from its neighbors, then
|
|
70
|
+
* flag the point closest to `centerValue` when it sits on a fragile island.
|
|
71
|
+
*/
|
|
72
|
+
export declare function analyzeSweepIsland(points: readonly SweepMetricPoint[], centerValue: number): IslandAnalysis;
|