desic-okx-agent 0.2.1 → 0.3.1

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.
Files changed (239) hide show
  1. package/README.en.md +90 -10
  2. package/README.md +76 -10
  3. package/dist/account/private-websocket.js +4 -4
  4. package/dist/account/private-websocket.js.map +1 -1
  5. package/dist/account/service.d.ts +12 -1
  6. package/dist/account/service.js +18 -0
  7. package/dist/account/service.js.map +1 -1
  8. package/dist/bars/rate-limiter.d.ts +18 -0
  9. package/dist/bars/rate-limiter.js +84 -0
  10. package/dist/bars/rate-limiter.js.map +1 -0
  11. package/dist/bars/schema.d.ts +36 -0
  12. package/dist/bars/schema.js +134 -0
  13. package/dist/bars/schema.js.map +1 -0
  14. package/dist/bars/service.d.ts +60 -0
  15. package/dist/bars/service.js +120 -0
  16. package/dist/bars/service.js.map +1 -0
  17. package/dist/bars/store.d.ts +105 -0
  18. package/dist/bars/store.js +415 -0
  19. package/dist/bars/store.js.map +1 -0
  20. package/dist/bars/timeframe.d.ts +40 -0
  21. package/dist/bars/timeframe.js +146 -0
  22. package/dist/bars/timeframe.js.map +1 -0
  23. package/dist/bars/types.d.ts +68 -0
  24. package/dist/bars/types.js +13 -0
  25. package/dist/bars/types.js.map +1 -0
  26. package/dist/cli/data-render.d.ts +37 -0
  27. package/dist/cli/data-render.js +143 -0
  28. package/dist/cli/data-render.js.map +1 -0
  29. package/dist/cli/doctor.js +7 -3
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/index.js +757 -26
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/live-render.d.ts +24 -0
  34. package/dist/cli/live-render.js +85 -0
  35. package/dist/cli/live-render.js.map +1 -0
  36. package/dist/cli/range.d.ts +28 -0
  37. package/dist/cli/range.js +63 -0
  38. package/dist/cli/range.js.map +1 -0
  39. package/dist/cli/render.js +3 -0
  40. package/dist/cli/render.js.map +1 -1
  41. package/dist/cli/strategy-render.d.ts +36 -0
  42. package/dist/cli/strategy-render.js +391 -0
  43. package/dist/cli/strategy-render.js.map +1 -0
  44. package/dist/cli/width.d.ts +18 -0
  45. package/dist/cli/width.js +71 -0
  46. package/dist/cli/width.js.map +1 -0
  47. package/dist/config/loader.js +1 -1
  48. package/dist/config/schema.d.ts +7 -0
  49. package/dist/config/schema.js +24 -0
  50. package/dist/config/schema.js.map +1 -1
  51. package/dist/core/okx-client.d.ts +9 -1
  52. package/dist/core/okx-client.js +14 -5
  53. package/dist/core/okx-client.js.map +1 -1
  54. package/dist/i18n/locale.d.ts +24 -0
  55. package/dist/i18n/locale.js +65 -0
  56. package/dist/i18n/locale.js.map +1 -0
  57. package/dist/i18n/messages.d.ts +333 -0
  58. package/dist/i18n/messages.js +660 -0
  59. package/dist/i18n/messages.js.map +1 -0
  60. package/dist/live/account-snapshot.d.ts +30 -0
  61. package/dist/live/account-snapshot.js +130 -0
  62. package/dist/live/account-snapshot.js.map +1 -0
  63. package/dist/live/cutoff-queue.d.ts +42 -0
  64. package/dist/live/cutoff-queue.js +69 -0
  65. package/dist/live/cutoff-queue.js.map +1 -0
  66. package/dist/live/execution-key.d.ts +23 -0
  67. package/dist/live/execution-key.js +31 -0
  68. package/dist/live/execution-key.js.map +1 -0
  69. package/dist/live/failures.d.ts +37 -0
  70. package/dist/live/failures.js +57 -0
  71. package/dist/live/failures.js.map +1 -0
  72. package/dist/live/gates.d.ts +65 -0
  73. package/dist/live/gates.js +136 -0
  74. package/dist/live/gates.js.map +1 -0
  75. package/dist/live/loop.d.ts +56 -0
  76. package/dist/live/loop.js +197 -0
  77. package/dist/live/loop.js.map +1 -0
  78. package/dist/live/preconditions.d.ts +48 -0
  79. package/dist/live/preconditions.js +69 -0
  80. package/dist/live/preconditions.js.map +1 -0
  81. package/dist/live/reconcile.d.ts +46 -0
  82. package/dist/live/reconcile.js +104 -0
  83. package/dist/live/reconcile.js.map +1 -0
  84. package/dist/live/runner.d.ts +57 -0
  85. package/dist/live/runner.js +160 -0
  86. package/dist/live/runner.js.map +1 -0
  87. package/dist/live/schema.d.ts +18 -0
  88. package/dist/live/schema.js +91 -0
  89. package/dist/live/schema.js.map +1 -0
  90. package/dist/live/service.d.ts +144 -0
  91. package/dist/live/service.js +303 -0
  92. package/dist/live/service.js.map +1 -0
  93. package/dist/live/session.d.ts +85 -0
  94. package/dist/live/session.js +234 -0
  95. package/dist/live/session.js.map +1 -0
  96. package/dist/live/sizing.d.ts +62 -0
  97. package/dist/live/sizing.js +79 -0
  98. package/dist/live/sizing.js.map +1 -0
  99. package/dist/live/store.d.ts +123 -0
  100. package/dist/live/store.js +350 -0
  101. package/dist/live/store.js.map +1 -0
  102. package/dist/live/types.d.ts +82 -0
  103. package/dist/live/types.js +2 -0
  104. package/dist/live/types.js.map +1 -0
  105. package/dist/market/websocket.d.ts +16 -1
  106. package/dist/market/websocket.js +60 -5
  107. package/dist/market/websocket.js.map +1 -1
  108. package/dist/mcp/server.d.ts +1 -0
  109. package/dist/mcp/server.js +15 -1
  110. package/dist/mcp/server.js.map +1 -1
  111. package/dist/network/connectivity.d.ts +9 -1
  112. package/dist/network/connectivity.js +28 -1
  113. package/dist/network/connectivity.js.map +1 -1
  114. package/dist/report/chart-script.d.ts +12 -0
  115. package/dist/report/chart-script.js +146 -0
  116. package/dist/report/chart-script.js.map +1 -0
  117. package/dist/report/compare-html.d.ts +8 -0
  118. package/dist/report/compare-html.js +254 -0
  119. package/dist/report/compare-html.js.map +1 -0
  120. package/dist/report/compare-script.d.ts +12 -0
  121. package/dist/report/compare-script.js +109 -0
  122. package/dist/report/compare-script.js.map +1 -0
  123. package/dist/report/compare.d.ts +61 -0
  124. package/dist/report/compare.js +205 -0
  125. package/dist/report/compare.js.map +1 -0
  126. package/dist/report/fetch.d.ts +20 -0
  127. package/dist/report/fetch.js +56 -0
  128. package/dist/report/fetch.js.map +1 -0
  129. package/dist/report/html.d.ts +54 -0
  130. package/dist/report/html.js +641 -0
  131. package/dist/report/html.js.map +1 -0
  132. package/dist/report/open.d.ts +42 -0
  133. package/dist/report/open.js +114 -0
  134. package/dist/report/open.js.map +1 -0
  135. package/dist/runtime/server.d.ts +16 -1
  136. package/dist/runtime/server.js +112 -9
  137. package/dist/runtime/server.js.map +1 -1
  138. package/dist/setup/installer.d.ts +1 -0
  139. package/dist/setup/installer.js +8 -0
  140. package/dist/setup/installer.js.map +1 -1
  141. package/dist/setup/wizard.js +19 -22
  142. package/dist/setup/wizard.js.map +1 -1
  143. package/dist/strategy/constants.d.ts +23 -0
  144. package/dist/strategy/constants.js +24 -0
  145. package/dist/strategy/constants.js.map +1 -0
  146. package/dist/strategy/environment.d.ts +52 -0
  147. package/dist/strategy/environment.js +187 -0
  148. package/dist/strategy/environment.js.map +1 -0
  149. package/dist/strategy/instrument.d.ts +29 -0
  150. package/dist/strategy/instrument.js +39 -0
  151. package/dist/strategy/instrument.js.map +1 -0
  152. package/dist/strategy/optimize.d.ts +73 -0
  153. package/dist/strategy/optimize.js +113 -0
  154. package/dist/strategy/optimize.js.map +1 -0
  155. package/dist/strategy/parameter-space.d.ts +59 -0
  156. package/dist/strategy/parameter-space.js +221 -0
  157. package/dist/strategy/parameter-space.js.map +1 -0
  158. package/dist/strategy/python-bridge.d.ts +24 -0
  159. package/dist/strategy/python-bridge.js +114 -0
  160. package/dist/strategy/python-bridge.js.map +1 -0
  161. package/dist/strategy/schema.d.ts +11 -0
  162. package/dist/strategy/schema.js +94 -0
  163. package/dist/strategy/schema.js.map +1 -0
  164. package/dist/strategy/series-codec.d.ts +50 -0
  165. package/dist/strategy/series-codec.js +103 -0
  166. package/dist/strategy/series-codec.js.map +1 -0
  167. package/dist/strategy/service.d.ts +138 -0
  168. package/dist/strategy/service.js +745 -0
  169. package/dist/strategy/service.js.map +1 -0
  170. package/dist/strategy/settings.d.ts +162 -0
  171. package/dist/strategy/settings.js +243 -0
  172. package/dist/strategy/settings.js.map +1 -0
  173. package/dist/strategy/store.d.ts +96 -0
  174. package/dist/strategy/store.js +369 -0
  175. package/dist/strategy/store.js.map +1 -0
  176. package/dist/strategy/templates.d.ts +11 -0
  177. package/dist/strategy/templates.js +134 -0
  178. package/dist/strategy/templates.js.map +1 -0
  179. package/dist/strategy/types.d.ts +111 -0
  180. package/dist/strategy/types.js +2 -0
  181. package/dist/strategy/types.js.map +1 -0
  182. package/dist/tools/catalog.d.ts +16 -0
  183. package/dist/tools/catalog.js +118 -17
  184. package/dist/tools/catalog.js.map +1 -1
  185. package/dist/trade/service.d.ts +12 -0
  186. package/dist/trade/service.js +24 -7
  187. package/dist/trade/service.js.map +1 -1
  188. package/dist/tui/app.d.ts +23 -0
  189. package/dist/tui/app.js +322 -0
  190. package/dist/tui/app.js.map +1 -0
  191. package/dist/tui/commands.d.ts +70 -0
  192. package/dist/tui/commands.js +313 -0
  193. package/dist/tui/commands.js.map +1 -0
  194. package/dist/tui/entries.d.ts +17 -0
  195. package/dist/tui/entries.js +24 -0
  196. package/dist/tui/entries.js.map +1 -0
  197. package/dist/tui/execute.d.ts +26 -0
  198. package/dist/tui/execute.js +664 -0
  199. package/dist/tui/execute.js.map +1 -0
  200. package/dist/tui/history.d.ts +17 -0
  201. package/dist/tui/history.js +48 -0
  202. package/dist/tui/history.js.map +1 -0
  203. package/dist/tui/index.d.ts +8 -0
  204. package/dist/tui/index.js +48 -0
  205. package/dist/tui/index.js.map +1 -0
  206. package/dist/tui/line-editor.d.ts +44 -0
  207. package/dist/tui/line-editor.js +98 -0
  208. package/dist/tui/line-editor.js.map +1 -0
  209. package/dist/tui/progress.d.ts +23 -0
  210. package/dist/tui/progress.js +46 -0
  211. package/dist/tui/progress.js.map +1 -0
  212. package/dist/tui/settings-editor.d.ts +18 -0
  213. package/dist/tui/settings-editor.js +115 -0
  214. package/dist/tui/settings-editor.js.map +1 -0
  215. package/docs/live-trading.md +455 -0
  216. package/docs/strategy-research.md +608 -0
  217. package/package.json +10 -1
  218. package/python/desic_strategy/__init__.py +34 -0
  219. package/python/desic_strategy/actions.py +158 -0
  220. package/python/desic_strategy/context.py +171 -0
  221. package/python/desic_strategy/engine.py +614 -0
  222. package/python/desic_strategy/indicators.py +159 -0
  223. package/python/desic_strategy/live.py +253 -0
  224. package/python/desic_strategy/policy.py +193 -0
  225. package/python/desic_strategy/portfolio.py +152 -0
  226. package/python/desic_strategy/report.py +319 -0
  227. package/python/desic_strategy/runner.py +574 -0
  228. package/python/desic_strategy/timeframe.py +150 -0
  229. package/python/main.py +18 -0
  230. package/skills/okx-live-trading/SKILL.md +117 -0
  231. package/skills/okx-live-trading/agents/openai.yaml +9 -0
  232. package/skills/okx-live-trading/references/lifecycle.md +128 -0
  233. package/skills/okx-strategy-research/SKILL.md +113 -0
  234. package/skills/okx-strategy-research/agents/openai.yaml +9 -0
  235. package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
  236. package/skills/okx-strategy-research/references/field-traps.md +142 -0
  237. package/skills/okx-strategy-research/references/python-api.md +125 -0
  238. package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
  239. package/skills/okx-trading/SKILL.md +11 -10
@@ -0,0 +1,113 @@
1
+ import { RuntimeError } from "../core/errors.js";
2
+ import { ONE_MINUTE_MS } from "../bars/types.js";
3
+ import { MINIMUM_ANNUALIZATION_DAYS } from "./constants.js";
4
+ /**
5
+ * Share of the window used to search. The rest is held back.
6
+ *
7
+ * Splitting is the whole point of the exercise rather than a refinement: the
8
+ * best parameters over any window are partly fitted to its noise, so a figure
9
+ * measured on the same bars that chose them is not evidence. 7:3 keeps enough
10
+ * bars to search over while leaving a segment long enough to disagree.
11
+ */
12
+ export const TRAIN_FRACTION = 0.7;
13
+ /**
14
+ * Smallest segment worth evaluating, in bars.
15
+ *
16
+ * A one-day validation segment produces a return that a single trade decides,
17
+ * and ranking on that is ranking on coincidence.
18
+ */
19
+ export const MINIMUM_SEGMENT_BARS = 1_440;
20
+ /**
21
+ * Splits an evaluation window into a training and a validation segment.
22
+ *
23
+ * The two are contiguous and disjoint: the validation segment begins on the
24
+ * minute after the training segment ends. Overlapping them by even one bar would
25
+ * let a candidate be credited for a bar it was chosen on.
26
+ */
27
+ export function splitWindow(evaluationStart, evaluationEnd) {
28
+ if (evaluationEnd <= evaluationStart) {
29
+ throw new RuntimeError("VALIDATION", "The optimization window must end after it starts");
30
+ }
31
+ const totalBars = Math.floor((evaluationEnd - evaluationStart) / ONE_MINUTE_MS) + 1;
32
+ const trainBars = Math.floor(totalBars * TRAIN_FRACTION);
33
+ const validationBars = totalBars - trainBars;
34
+ if (trainBars < MINIMUM_SEGMENT_BARS || validationBars < MINIMUM_SEGMENT_BARS) {
35
+ const days = ((MINIMUM_SEGMENT_BARS / TRAIN_FRACTION) * ONE_MINUTE_MS) / 86_400_000;
36
+ throw new RuntimeError("VALIDATION", `A ${TRAIN_FRACTION * 100}/${Math.round((1 - TRAIN_FRACTION) * 100)} split of ${totalBars} bars leaves segments below the ${MINIMUM_SEGMENT_BARS}-bar minimum. Use a window of at least ${Math.ceil(days)} days.`, false, { totalBars, trainBars, validationBars, minimumBars: MINIMUM_SEGMENT_BARS });
37
+ }
38
+ const trainEnd = evaluationStart + (trainBars - 1) * ONE_MINUTE_MS;
39
+ return {
40
+ trainStart: evaluationStart,
41
+ trainEnd,
42
+ validationStart: trainEnd + ONE_MINUTE_MS,
43
+ validationEnd: evaluationEnd,
44
+ trainBars,
45
+ validationBars
46
+ };
47
+ }
48
+ /**
49
+ * Scores one candidate from its validation-segment metrics.
50
+ *
51
+ * Calmar is the intended measure, but it divides an annualized return by
52
+ * drawdown, and the engine withholds annualized figures below
53
+ * `MINIMUM_ANNUALIZATION_DAYS` because extrapolating a short window produces
54
+ * five-digit percentages that read as precision and carry none. Rather than fall
55
+ * back silently, the basis used travels with the score and is printed beside the
56
+ * table: a column headed "calmar" that is sometimes a different quantity is worse
57
+ * than an honest label.
58
+ *
59
+ * Every basis keeps the sign of the return, so a losing candidate can never
60
+ * outrank a profitable one through a small denominator.
61
+ */
62
+ export function rankCandidate(metrics) {
63
+ if (!metrics)
64
+ return { score: Number.NEGATIVE_INFINITY, basis: "return" };
65
+ const calmar = numeric(metrics.calmar);
66
+ if (calmar !== null && metrics.annualizationReliable === true) {
67
+ return { score: calmar, basis: "calmar" };
68
+ }
69
+ const returnPct = numeric(metrics.returnPct) ?? 0;
70
+ const drawdownPct = numeric(metrics.maxDrawdownPct) ?? 0;
71
+ // A run that never drew down has no risk measure to divide by. Ranking it by
72
+ // return alone is the honest reading, and it is labelled as such.
73
+ if (drawdownPct <= 0)
74
+ return { score: returnPct, basis: "return" };
75
+ return { score: returnPct / drawdownPct, basis: "return_over_drawdown" };
76
+ }
77
+ /** Message-bundle-free description of a basis, for the report footer. */
78
+ export function describeRankBasis(basis, validationDays) {
79
+ switch (basis) {
80
+ case "calmar":
81
+ return "ranked by validation Calmar (annualized return / max drawdown)";
82
+ case "return_over_drawdown":
83
+ return `ranked by validation return / max drawdown; Calmar needs ${MINIMUM_ANNUALIZATION_DAYS}+ days and this segment is ${validationDays.toFixed(1)}`;
84
+ default:
85
+ return "ranked by validation return; no candidate drew down, so there is no risk denominator";
86
+ }
87
+ }
88
+ export function verdictFor(train, validation) {
89
+ if (!train || !validation)
90
+ return "failed";
91
+ const trainReturn = numeric(train.returnPct) ?? 0;
92
+ const validationReturn = numeric(validation.returnPct) ?? 0;
93
+ if (trainReturn > 0 && validationReturn <= 0)
94
+ return "overfit";
95
+ if (validationReturn <= 0)
96
+ return "weak";
97
+ // Keeping less than a third of the training edge is decay worth flagging even
98
+ // though the sign held.
99
+ if (trainReturn > 0 && validationReturn < trainReturn / 3)
100
+ return "overfit";
101
+ // Profitable only on the segment it was not chosen on. Nothing was carried
102
+ // across the split, so there is nothing the split confirmed.
103
+ if (trainReturn <= 0)
104
+ return "unexplained";
105
+ return "holds";
106
+ }
107
+ function numeric(value) {
108
+ if (value === null || value === undefined)
109
+ return null;
110
+ const parsed = Number(value);
111
+ return Number.isFinite(parsed) ? parsed : null;
112
+ }
113
+ //# sourceMappingURL=optimize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimize.js","sourceRoot":"","sources":["../../src/strategy/optimize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /** One tunable parameter's inclusive range, and the step between candidates. */
2
+ export interface ParameterRange {
3
+ min: number;
4
+ max: number;
5
+ step: number;
6
+ }
7
+ export type ParameterSpace = Record<string, ParameterRange>;
8
+ /**
9
+ * Ceiling on the full cartesian product.
10
+ *
11
+ * A wide space is almost always an accident rather than an intent: three
12
+ * parameters at ten values each is already a thousand candidates, and every one
13
+ * of them is simulated twice. Refusing is more useful than starting something
14
+ * that finishes in hours.
15
+ */
16
+ export declare const MAX_COMBINATIONS = 5000;
17
+ /** Below two candidates there is nothing to compare. */
18
+ export declare const MIN_COMBINATIONS = 2;
19
+ /** Candidate budget used when a caller does not choose one. */
20
+ export declare const DEFAULT_BUDGET = 100;
21
+ /** Largest budget accepted. Beyond this the wait outgrows the interactive loop. */
22
+ export declare const MAX_BUDGET = 1000;
23
+ /**
24
+ * Validates a declared space against the parameters the strategy actually reads.
25
+ *
26
+ * A range for a parameter the strategy ignores is silently useless: every
27
+ * candidate behaves identically, and whichever one ranks first is noise
28
+ * presented as a finding. That is rejected rather than dropped, which is why
29
+ * this needs the strategy's own parameter defaults to check against.
30
+ */
31
+ export declare function parseParameterSpace(raw: unknown, strategyParams: Record<string, unknown>): ParameterSpace;
32
+ /**
33
+ * Every value one parameter takes, inclusive of both bounds.
34
+ *
35
+ * Each value is computed from the index rather than by repeated addition, and
36
+ * rounded, because accumulating a step such as 0.1 drifts into values like
37
+ * 2.7999999999999996 that then appear verbatim in the report.
38
+ */
39
+ export declare function valuesOf(range: ParameterRange): number[];
40
+ export declare function countCombinations(space: ParameterSpace): number;
41
+ /** Describes a space in one line, for the run summary and the report header. */
42
+ export declare function describeSpace(space: ParameterSpace): string;
43
+ /** Keys in a fixed order, so candidate indices mean the same thing on a rerun. */
44
+ export declare function sortedKeys(space: ParameterSpace): string[];
45
+ /**
46
+ * Chooses which combinations to evaluate.
47
+ *
48
+ * Deterministic by construction: the seed is derived from the space and the
49
+ * budget, so the same request always produces the same candidate set. A rerun is
50
+ * then comparable to the original rather than a fresh lottery — the same reason a
51
+ * run records a `dataSnapshotId`.
52
+ *
53
+ * When the product exceeds the budget this is a Latin hypercube rather than plain
54
+ * random sampling: each parameter's values are dealt out across the slots and
55
+ * shuffled independently, so every value of every parameter is evaluated at least
56
+ * once. Random sampling can leave one untried, and a search that never evaluated
57
+ * `slowPeriod: 200` cannot report anything about it.
58
+ */
59
+ export declare function sampleCandidates(space: ParameterSpace, budget: number): Array<Record<string, number>>;
@@ -0,0 +1,221 @@
1
+ import { RuntimeError } from "../core/errors.js";
2
+ /**
3
+ * Ceiling on the full cartesian product.
4
+ *
5
+ * A wide space is almost always an accident rather than an intent: three
6
+ * parameters at ten values each is already a thousand candidates, and every one
7
+ * of them is simulated twice. Refusing is more useful than starting something
8
+ * that finishes in hours.
9
+ */
10
+ export const MAX_COMBINATIONS = 5_000;
11
+ /** Below two candidates there is nothing to compare. */
12
+ export const MIN_COMBINATIONS = 2;
13
+ /** Candidate budget used when a caller does not choose one. */
14
+ export const DEFAULT_BUDGET = 100;
15
+ /** Largest budget accepted. Beyond this the wait outgrows the interactive loop. */
16
+ export const MAX_BUDGET = 1_000;
17
+ /**
18
+ * Validates a declared space against the parameters the strategy actually reads.
19
+ *
20
+ * A range for a parameter the strategy ignores is silently useless: every
21
+ * candidate behaves identically, and whichever one ranks first is noise
22
+ * presented as a finding. That is rejected rather than dropped, which is why
23
+ * this needs the strategy's own parameter defaults to check against.
24
+ */
25
+ export function parseParameterSpace(raw, strategyParams) {
26
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
27
+ throw new RuntimeError("VALIDATION", "A parameter space must be an object of { min, max, step } ranges, keyed by parameter name");
28
+ }
29
+ const entries = Object.entries(raw);
30
+ if (entries.length === 0) {
31
+ throw new RuntimeError("VALIDATION", "A parameter space needs at least one parameter");
32
+ }
33
+ const space = {};
34
+ for (const [key, value] of entries) {
35
+ if (!Object.hasOwn(strategyParams, key)) {
36
+ // An empty baseline is the common case, and "the strategy declares no
37
+ // parameter" reads as a fault in the strategy when the real cause is a
38
+ // missing --params file. Name the actual omission.
39
+ const available = Object.keys(strategyParams).sort();
40
+ throw new RuntimeError("VALIDATION", available.length === 0
41
+ ? `No baseline parameters were provided, so '${key}' cannot be tuned. Pass the strategy's own defaults with --params: a space is checked against them, because tuning a key the strategy never reads leaves every candidate identical.`
42
+ : `The strategy declares no parameter '${key}', so tuning it would change nothing`, false, { available });
43
+ }
44
+ const current = strategyParams[key];
45
+ if (typeof current !== "number" || !Number.isFinite(current)) {
46
+ throw new RuntimeError("VALIDATION", `Parameter '${key}' is not a finite number, so it has no numeric range to search`);
47
+ }
48
+ space[key] = parseRange(key, value);
49
+ }
50
+ const total = countCombinations(space);
51
+ if (total < MIN_COMBINATIONS) {
52
+ throw new RuntimeError("VALIDATION", `This space yields ${total} candidate, and at least ${MIN_COMBINATIONS} are needed to compare anything`);
53
+ }
54
+ if (total > MAX_COMBINATIONS) {
55
+ throw new RuntimeError("VALIDATION", `This space yields ${total} combinations, above the ${MAX_COMBINATIONS} limit. Narrow a range or widen a step.`, false, { combinations: total, limit: MAX_COMBINATIONS });
56
+ }
57
+ return space;
58
+ }
59
+ function parseRange(key, value) {
60
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
61
+ throw new RuntimeError("VALIDATION", `The range for '${key}' must be an object with min, max, and step`);
62
+ }
63
+ const record = value;
64
+ const min = finite(record.min, `${key}.min`);
65
+ const max = finite(record.max, `${key}.max`);
66
+ const step = finite(record.step, `${key}.step`);
67
+ if (step <= 0)
68
+ throw new RuntimeError("VALIDATION", `${key}.step must be greater than zero`);
69
+ if (max < min)
70
+ throw new RuntimeError("VALIDATION", `${key}.max must be at or above ${key}.min`);
71
+ if (max > min && step > max - min) {
72
+ throw new RuntimeError("VALIDATION", `${key}.step of ${step} exceeds its ${min}..${max} range, so only one value would ever be tried`);
73
+ }
74
+ return { min, max, step };
75
+ }
76
+ function finite(value, label) {
77
+ // `Number("")` is 0 and `Number(null)` is 0, so both are rejected explicitly:
78
+ // an omitted bound must not silently become a legitimate zero.
79
+ if (value === undefined || value === null || value === "") {
80
+ throw new RuntimeError("VALIDATION", `${label} is required and must be a finite number`);
81
+ }
82
+ const parsed = Number(value);
83
+ if (!Number.isFinite(parsed)) {
84
+ throw new RuntimeError("VALIDATION", `${label} must be a finite number`);
85
+ }
86
+ return parsed;
87
+ }
88
+ /**
89
+ * Every value one parameter takes, inclusive of both bounds.
90
+ *
91
+ * Each value is computed from the index rather than by repeated addition, and
92
+ * rounded, because accumulating a step such as 0.1 drifts into values like
93
+ * 2.7999999999999996 that then appear verbatim in the report.
94
+ */
95
+ export function valuesOf(range) {
96
+ const count = Math.floor((range.max - range.min) / range.step + 1e-9);
97
+ return Array.from({ length: count + 1 }, (_, index) => Number((range.min + index * range.step).toFixed(10)));
98
+ }
99
+ export function countCombinations(space) {
100
+ return Object.values(space).reduce((total, range) => total * valuesOf(range).length, 1);
101
+ }
102
+ /** Describes a space in one line, for the run summary and the report header. */
103
+ export function describeSpace(space) {
104
+ return sortedKeys(space)
105
+ .map((key) => `${key} ${space[key].min}..${space[key].max} step ${space[key].step}`)
106
+ .join(", ");
107
+ }
108
+ /** Keys in a fixed order, so candidate indices mean the same thing on a rerun. */
109
+ export function sortedKeys(space) {
110
+ return Object.keys(space).sort();
111
+ }
112
+ /**
113
+ * Chooses which combinations to evaluate.
114
+ *
115
+ * Deterministic by construction: the seed is derived from the space and the
116
+ * budget, so the same request always produces the same candidate set. A rerun is
117
+ * then comparable to the original rather than a fresh lottery — the same reason a
118
+ * run records a `dataSnapshotId`.
119
+ *
120
+ * When the product exceeds the budget this is a Latin hypercube rather than plain
121
+ * random sampling: each parameter's values are dealt out across the slots and
122
+ * shuffled independently, so every value of every parameter is evaluated at least
123
+ * once. Random sampling can leave one untried, and a search that never evaluated
124
+ * `slowPeriod: 200` cannot report anything about it.
125
+ */
126
+ export function sampleCandidates(space, budget) {
127
+ if (!Number.isInteger(budget) || budget < MIN_COMBINATIONS) {
128
+ throw new RuntimeError("VALIDATION", `The candidate budget must be a whole number of at least ${MIN_COMBINATIONS}`);
129
+ }
130
+ const keys = sortedKeys(space);
131
+ const grids = keys.map((key) => valuesOf(space[key]));
132
+ const total = grids.reduce((product, values) => product * values.length, 1);
133
+ if (total <= budget) {
134
+ return Array.from({ length: total }, (_, index) => combinationAt(keys, grids, index));
135
+ }
136
+ // Covering every value of the widest parameter takes at least that many
137
+ // candidates. Below it the guarantee is arithmetically impossible, so the
138
+ // budget is refused rather than quietly weakened.
139
+ const widest = Math.max(...grids.map((values) => values.length));
140
+ if (budget < widest) {
141
+ const wide = keys[grids.findIndex((values) => values.length === widest)];
142
+ throw new RuntimeError("VALIDATION", `A budget of ${budget} cannot cover '${wide}', which has ${widest} values. Use a budget of at least ${widest}, or widen that step.`, false, { budget, required: widest, parameter: wide });
143
+ }
144
+ const random = seededRandom(`${JSON.stringify(orderedSpace(space, keys))}|${budget}`);
145
+ // One shuffled column per parameter: value indices dealt round-robin across the
146
+ // slots, then permuted. Independent shuffles keep the axes from pairing up
147
+ // diagonally, which would only ever test fast and slow periods in lockstep.
148
+ const columns = grids.map((values) => {
149
+ const slots = Array.from({ length: budget }, (_, slot) => slot % values.length);
150
+ for (let index = slots.length - 1; index > 0; index -= 1) {
151
+ const swap = Math.floor(random() * (index + 1));
152
+ [slots[index], slots[swap]] = [slots[swap], slots[index]];
153
+ }
154
+ return slots;
155
+ });
156
+ const chosen = [];
157
+ const seen = new Set();
158
+ const add = (candidate) => {
159
+ const signature = keys.map((key) => candidate[key]).join("|");
160
+ // Dropping a duplicate cannot lose coverage: an identical candidate carrying
161
+ // the same values is already kept.
162
+ if (seen.has(signature))
163
+ return;
164
+ seen.add(signature);
165
+ chosen.push(candidate);
166
+ };
167
+ for (let slot = 0; slot < budget; slot += 1) {
168
+ const candidate = {};
169
+ keys.forEach((key, axis) => {
170
+ candidate[key] = grids[axis][columns[axis][slot]];
171
+ });
172
+ add(candidate);
173
+ }
174
+ // Collisions leave the budget short. Top up by walking the product with a
175
+ // stride so the extra candidates spread out instead of clustering at index 0,
176
+ // then close any remainder in order — both passes stay deterministic.
177
+ const stride = Math.max(1, Math.floor(total / budget));
178
+ for (let index = 0; index < total && chosen.length < budget; index += stride) {
179
+ add(combinationAt(keys, grids, index));
180
+ }
181
+ for (let index = 0; index < total && chosen.length < budget; index += 1) {
182
+ add(combinationAt(keys, grids, index));
183
+ }
184
+ return chosen;
185
+ }
186
+ /** Mixed-radix decoding of a flat index into one combination. */
187
+ function combinationAt(keys, grids, index) {
188
+ const candidate = {};
189
+ let remainder = index;
190
+ for (let axis = keys.length - 1; axis >= 0; axis -= 1) {
191
+ const values = grids[axis];
192
+ candidate[keys[axis]] = values[remainder % values.length];
193
+ remainder = Math.floor(remainder / values.length);
194
+ }
195
+ return candidate;
196
+ }
197
+ function orderedSpace(space, keys) {
198
+ return keys.map((key) => [key, space[key]]);
199
+ }
200
+ /**
201
+ * Small deterministic generator, seeded from text.
202
+ *
203
+ * `Math.random` cannot be used here: the candidate set has to be reproducible
204
+ * from the request alone. FNV-1a then mulberry32, both fixed integer arithmetic,
205
+ * so the sequence is identical on every platform.
206
+ */
207
+ function seededRandom(seed) {
208
+ let hash = 0x811c9dc5;
209
+ for (let index = 0; index < seed.length; index += 1) {
210
+ hash ^= seed.charCodeAt(index);
211
+ hash = Math.imul(hash, 0x01000193);
212
+ }
213
+ let state = hash >>> 0;
214
+ return () => {
215
+ state = (state + 0x6d2b79f5) >>> 0;
216
+ let value = Math.imul(state ^ (state >>> 15), 1 | state);
217
+ value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
218
+ return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
219
+ };
220
+ }
221
+ //# sourceMappingURL=parameter-space.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parameter-space.js","sourceRoot":"","sources":["../../src/strategy/parameter-space.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ export interface PythonInvocation {
2
+ interpreter: string;
3
+ engineRoot: string;
4
+ request: unknown;
5
+ timeoutMs?: number;
6
+ onProgress?: (progress: {
7
+ done: number;
8
+ total: number;
9
+ pct: number;
10
+ }) => void;
11
+ signal?: AbortSignal;
12
+ }
13
+ export interface PythonResponse {
14
+ ok: boolean;
15
+ [key: string]: unknown;
16
+ }
17
+ /**
18
+ * Runs one strategy request in a short-lived Python process.
19
+ *
20
+ * `-I` is deliberate: it ignores PYTHONPATH, the user site directory, and other
21
+ * ambient configuration, so a run cannot be redirected by the environment.
22
+ * `-u` keeps progress lines flowing instead of sitting in a pipe buffer.
23
+ */
24
+ export declare function invokePython(invocation: PythonInvocation): Promise<PythonResponse>;
@@ -0,0 +1,114 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { RuntimeError } from "../core/errors.js";
4
+ /** stdout is a single JSON object; a larger reply means something is wrong. */
5
+ const MAX_STDOUT_BYTES = 64 * 1024 * 1024;
6
+ const MAX_STDERR_BYTES = 1 * 1024 * 1024;
7
+ /**
8
+ * Runs one strategy request in a short-lived Python process.
9
+ *
10
+ * `-I` is deliberate: it ignores PYTHONPATH, the user site directory, and other
11
+ * ambient configuration, so a run cannot be redirected by the environment.
12
+ * `-u` keeps progress lines flowing instead of sitting in a pipe buffer.
13
+ */
14
+ export async function invokePython(invocation) {
15
+ const { interpreter, engineRoot, request, onProgress, signal } = invocation;
16
+ const timeoutMs = invocation.timeoutMs ?? 30 * 60_000;
17
+ const entry = path.join(engineRoot, "main.py");
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(interpreter, ["-I", "-u", entry], {
20
+ stdio: ["pipe", "pipe", "pipe"],
21
+ // A strategy has no business reading the repository it was launched from.
22
+ cwd: engineRoot
23
+ });
24
+ const stdout = [];
25
+ let stdoutBytes = 0;
26
+ let stderrText = "";
27
+ let stderrBuffer = "";
28
+ let settled = false;
29
+ let timedOut = false;
30
+ const finish = (error, value) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ clearTimeout(timer);
35
+ signal?.removeEventListener("abort", onAbort);
36
+ if (error)
37
+ reject(error);
38
+ else
39
+ resolve(value);
40
+ };
41
+ const timer = setTimeout(() => {
42
+ timedOut = true;
43
+ child.kill("SIGKILL");
44
+ }, timeoutMs);
45
+ const onAbort = () => {
46
+ child.kill("SIGKILL");
47
+ };
48
+ signal?.addEventListener("abort", onAbort, { once: true });
49
+ child.stdout.on("data", (chunk) => {
50
+ stdoutBytes += chunk.length;
51
+ if (stdoutBytes > MAX_STDOUT_BYTES) {
52
+ child.kill("SIGKILL");
53
+ finish(new RuntimeError("INTERNAL", "The strategy engine produced an oversized response"));
54
+ return;
55
+ }
56
+ stdout.push(chunk);
57
+ });
58
+ child.stderr.on("data", (chunk) => {
59
+ stderrBuffer += chunk.toString("utf8");
60
+ const lines = stderrBuffer.split("\n");
61
+ stderrBuffer = lines.pop() ?? "";
62
+ for (const line of lines) {
63
+ if (line.startsWith("PROGRESS ")) {
64
+ try {
65
+ const parsed = JSON.parse(line.slice("PROGRESS ".length));
66
+ onProgress?.({
67
+ done: Number(parsed.done ?? 0),
68
+ total: Number(parsed.total ?? 0),
69
+ pct: Number(parsed.pct ?? 0)
70
+ });
71
+ }
72
+ catch {
73
+ // A malformed progress line is diagnostic noise, not a failure.
74
+ }
75
+ continue;
76
+ }
77
+ // Keep stderr: discarding it makes an unexpected exit undiagnosable.
78
+ if (stderrText.length < MAX_STDERR_BYTES)
79
+ stderrText += `${line}\n`;
80
+ }
81
+ });
82
+ child.on("error", (error) => {
83
+ finish(new RuntimeError("CAPABILITY_UNAVAILABLE", `Could not start the Python strategy engine (${interpreter}): ${error.message}`));
84
+ });
85
+ child.on("close", (code, killSignal) => {
86
+ if (timedOut) {
87
+ finish(new RuntimeError("INTERNAL", `The strategy engine exceeded its ${timeoutMs}ms time limit`));
88
+ return;
89
+ }
90
+ if (signal?.aborted) {
91
+ finish(new RuntimeError("INTERNAL", "The strategy run was cancelled"));
92
+ return;
93
+ }
94
+ const text = Buffer.concat(stdout).toString("utf8").trim();
95
+ if (!text) {
96
+ const detail = stderrText.trim() || `exit code ${code}${killSignal ? ` (${killSignal})` : ""}`;
97
+ finish(new RuntimeError("INTERNAL", `The strategy engine produced no output: ${detail}`));
98
+ return;
99
+ }
100
+ try {
101
+ finish(null, JSON.parse(text));
102
+ }
103
+ catch {
104
+ finish(new RuntimeError("INTERNAL", `The strategy engine returned invalid JSON: ${text.slice(0, 200)}`));
105
+ }
106
+ });
107
+ child.stdin.on("error", () => {
108
+ // The close handler reports the real cause; a broken pipe here is a
109
+ // symptom of the child having already exited.
110
+ });
111
+ child.stdin.end(JSON.stringify(request));
112
+ });
113
+ }
114
+ //# sourceMappingURL=python-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-bridge.js","sourceRoot":"","sources":["../../src/strategy/python-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Storage for queued and completed strategy runs.
3
+ *
4
+ * The equity curve is stored as chunked binary float64 blocks rather than JSON rows.
5
+ * Every value round-trips bit-for-bit, which decimal JSON text does not guarantee, and
6
+ * a measured 344k-point curve is 2.4 MB here against roughly 40 MB as JSON objects.
7
+ *
8
+ * `codec` names the layout of each chunk's payload. It is read rather than inferred so
9
+ * that chunks written by an earlier build keep decoding correctly — see `series-codec.ts`.
10
+ */
11
+ export declare const STRATEGY_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS strategy_runs (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n status TEXT NOT NULL,\n strategy_name TEXT NOT NULL,\n strategy_hash TEXT NOT NULL,\n strategy_source TEXT NOT NULL,\n inst_id TEXT NOT NULL,\n request_json TEXT NOT NULL,\n data_snapshot_id TEXT,\n bar_count INTEGER NOT NULL DEFAULT 0,\n preload_bars INTEGER NOT NULL DEFAULT 0,\n progress_pct REAL NOT NULL DEFAULT 0,\n eta_ms INTEGER,\n metrics_json TEXT,\n -- The assumptions the run actually used. Stored per run because settings can\n -- change between runs, and a report that cannot say what it assumed is not\n -- comparable to any other report.\n assumptions_json TEXT,\n error TEXT,\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_strategy_runs_created\n ON strategy_runs(created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_strategy_runs_status\n ON strategy_runs(status, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS strategy_run_series (\n run_id TEXT NOT NULL,\n chunk_index INTEGER NOT NULL,\n from_point INTEGER NOT NULL,\n to_point INTEGER NOT NULL,\n start_ms INTEGER NOT NULL,\n step_ms INTEGER NOT NULL,\n payload BLOB NOT NULL,\n codec TEXT NOT NULL DEFAULT 'f64x4',\n PRIMARY KEY(run_id, chunk_index)\n) WITHOUT ROWID;\n\nCREATE TABLE IF NOT EXISTS strategy_run_trades (\n run_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n trade_json TEXT NOT NULL,\n PRIMARY KEY(run_id, seq)\n) WITHOUT ROWID;\n\nCREATE TABLE IF NOT EXISTS strategy_run_actions (\n run_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n action_json TEXT NOT NULL,\n PRIMARY KEY(run_id, seq)\n) WITHOUT ROWID;\n\n-- One row per evaluated parameter combination of an optimize run.\n--\n-- Training and validation metrics are stored separately rather than merged into\n-- one figure. The gap between them is the finding: a candidate that earned its\n-- ranking and one that memorized the training segment are indistinguishable from\n-- a single number.\nCREATE TABLE IF NOT EXISTS strategy_optimization_candidates (\n run_id TEXT NOT NULL,\n candidate_index INTEGER NOT NULL,\n params_json TEXT NOT NULL,\n status TEXT NOT NULL,\n train_metrics_json TEXT,\n validation_metrics_json TEXT,\n -- Validation-segment score. Null while queued, and for a candidate that failed.\n rank_score REAL,\n -- Which formula produced rank_score, because it is not always Calmar.\n rank_basis TEXT,\n verdict TEXT,\n error TEXT,\n PRIMARY KEY(run_id, candidate_index)\n) WITHOUT ROWID;\n\n-- Ranking reads this in score order, so it is indexed rather than sorted per read.\nCREATE INDEX IF NOT EXISTS idx_optimization_rank\n ON strategy_optimization_candidates(run_id, rank_score DESC);\n";