opentool 0.20.1 → 0.21.0
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/adapters/hyperliquid/browser.d.ts +2 -2
- package/dist/adapters/hyperliquid/index.d.ts +3 -3
- package/dist/adapters/polymarket/index.d.ts +1 -1
- package/dist/{browser-z97Ptt32.d.ts → browser-Bf-eTSwj.d.ts} +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1262 -5
- package/dist/index.js.map +1 -1
- package/dist/mpp/index.d.ts +64 -0
- package/dist/mpp/index.js +75 -0
- package/dist/mpp/index.js.map +1 -0
- package/dist/quant/index.d.ts +579 -0
- package/dist/quant/index.js +1103 -0
- package/dist/quant/index.js.map +1 -0
- package/dist/{types-BaTmu0gS.d.ts → types-D8s9zx-U.d.ts} +18 -1
- package/dist/wallet/browser.d.ts +1 -1
- package/dist/wallet/browser.js +27 -1
- package/dist/wallet/browser.js.map +1 -1
- package/dist/wallet/index.d.ts +2 -2
- package/dist/wallet/index.js +96 -4
- package/dist/wallet/index.js.map +1 -1
- package/package.json +11 -2
- package/templates/base/package.json +1 -1
- package/templates/polymarket-simple-trade/package.json +1 -1
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// src/quant/schemas.ts
|
|
4
|
+
var quantStrategyFamilySchema = z.enum([
|
|
5
|
+
"benchmark",
|
|
6
|
+
"signal_rule",
|
|
7
|
+
"trend",
|
|
8
|
+
"mean_reversion",
|
|
9
|
+
"stat_arb",
|
|
10
|
+
"carry",
|
|
11
|
+
"hedge",
|
|
12
|
+
"execution",
|
|
13
|
+
"market_making",
|
|
14
|
+
"options",
|
|
15
|
+
"regime_ml",
|
|
16
|
+
"prediction_market",
|
|
17
|
+
"defi_lp"
|
|
18
|
+
]);
|
|
19
|
+
var quantTestKindSchema = z.enum([
|
|
20
|
+
"prompt_plan_check",
|
|
21
|
+
"data_availability",
|
|
22
|
+
"signal_study",
|
|
23
|
+
"idea_rule_simulation",
|
|
24
|
+
"variant_sensitivity",
|
|
25
|
+
"leakage_check",
|
|
26
|
+
"cost_stress"
|
|
27
|
+
]);
|
|
28
|
+
var quantResolutionSchema = z.enum(["1", "5", "15", "30", "60", "240", "1D", "1W"]);
|
|
29
|
+
var quantBarSchema = z.object({
|
|
30
|
+
time: z.number().int().nonnegative(),
|
|
31
|
+
open: z.number().positive(),
|
|
32
|
+
high: z.number().positive(),
|
|
33
|
+
low: z.number().positive(),
|
|
34
|
+
close: z.number().positive(),
|
|
35
|
+
volume: z.number().nonnegative().optional(),
|
|
36
|
+
fundingRate: z.number().optional()
|
|
37
|
+
}).strict();
|
|
38
|
+
var quantFeatureSpecSchema = z.object({
|
|
39
|
+
id: z.string().min(1),
|
|
40
|
+
kind: z.string().min(1),
|
|
41
|
+
params: z.record(z.string(), z.unknown()).default({})
|
|
42
|
+
}).strict();
|
|
43
|
+
var quantRuleSpecSchema = z.object({
|
|
44
|
+
kind: z.string().min(1),
|
|
45
|
+
params: z.record(z.string(), z.unknown()).default({})
|
|
46
|
+
}).strict();
|
|
47
|
+
var quantIdeaSpecV1Schema = z.object({
|
|
48
|
+
version: z.literal("1"),
|
|
49
|
+
family: quantStrategyFamilySchema,
|
|
50
|
+
thesis: z.object({
|
|
51
|
+
title: z.string().min(1),
|
|
52
|
+
belief: z.string().min(1),
|
|
53
|
+
expectedDirection: z.enum([
|
|
54
|
+
"up",
|
|
55
|
+
"down",
|
|
56
|
+
"relative",
|
|
57
|
+
"mean_revert",
|
|
58
|
+
"volatility",
|
|
59
|
+
"carry",
|
|
60
|
+
"hedge",
|
|
61
|
+
"unknown"
|
|
62
|
+
]),
|
|
63
|
+
horizon: z.array(z.string().min(1)).min(1)
|
|
64
|
+
}).strict(),
|
|
65
|
+
market: z.object({
|
|
66
|
+
venue: z.enum(["hyperliquid", "polymarket", "derive", "external_fixture"]),
|
|
67
|
+
symbol: z.string().min(1).optional(),
|
|
68
|
+
universe: z.array(z.string().min(1)).optional()
|
|
69
|
+
}).strict(),
|
|
70
|
+
requiredSources: z.array(z.string().min(1)).default([]),
|
|
71
|
+
features: z.array(quantFeatureSpecSchema).default([]),
|
|
72
|
+
rule: quantRuleSpecSchema.optional(),
|
|
73
|
+
risk: z.object({
|
|
74
|
+
maxPositionUsd: z.number().positive().optional(),
|
|
75
|
+
maxLeverage: z.number().positive().optional(),
|
|
76
|
+
stopLossPct: z.number().positive().optional(),
|
|
77
|
+
takeProfitPct: z.number().positive().optional(),
|
|
78
|
+
maxDrawdownPct: z.number().positive().optional()
|
|
79
|
+
}).strict().optional()
|
|
80
|
+
}).strict();
|
|
81
|
+
var quantTestRequestV1Schema = z.object({
|
|
82
|
+
version: z.literal("1"),
|
|
83
|
+
idea: quantIdeaSpecV1Schema,
|
|
84
|
+
testKinds: z.array(quantTestKindSchema).min(1),
|
|
85
|
+
window: z.object({
|
|
86
|
+
resolution: quantResolutionSchema,
|
|
87
|
+
timeframeStart: z.string().min(1),
|
|
88
|
+
timeframeEnd: z.string().min(1),
|
|
89
|
+
warmupBars: z.number().int().nonnegative().optional()
|
|
90
|
+
}).strict(),
|
|
91
|
+
assumptions: z.object({
|
|
92
|
+
initialEquityUsd: z.number().positive().optional(),
|
|
93
|
+
makerFeeBps: z.number().nonnegative().optional(),
|
|
94
|
+
takerFeeBps: z.number().nonnegative().optional(),
|
|
95
|
+
slippageBps: z.number().nonnegative().optional(),
|
|
96
|
+
fundingModel: z.enum(["ignore", "historical", "estimated"]).optional()
|
|
97
|
+
}).strict().default({}),
|
|
98
|
+
variantSpace: z.record(z.string(), z.unknown()).optional()
|
|
99
|
+
}).strict();
|
|
100
|
+
var quantDecisionActionSchema = z.enum(["hold", "flat", "long", "short", "exit"]);
|
|
101
|
+
var quantDecisionSchema = z.object({
|
|
102
|
+
time: z.number().int().nonnegative(),
|
|
103
|
+
symbol: z.string().min(1),
|
|
104
|
+
action: quantDecisionActionSchema,
|
|
105
|
+
targetPosition: z.number(),
|
|
106
|
+
reason: z.string().min(1),
|
|
107
|
+
price: z.number().positive().optional()
|
|
108
|
+
}).strict();
|
|
109
|
+
var quantDecisionArtifactV1Schema = z.object({
|
|
110
|
+
version: z.literal("1"),
|
|
111
|
+
family: quantStrategyFamilySchema,
|
|
112
|
+
symbol: z.string().min(1),
|
|
113
|
+
resolution: quantResolutionSchema,
|
|
114
|
+
decisions: z.array(quantDecisionSchema),
|
|
115
|
+
warnings: z.array(z.string()).default([])
|
|
116
|
+
}).strict();
|
|
117
|
+
var quantTesterReportV1Schema = z.object({
|
|
118
|
+
ok: z.boolean(),
|
|
119
|
+
testRunKind: z.enum([
|
|
120
|
+
"prompt_plan_check",
|
|
121
|
+
"data_availability",
|
|
122
|
+
"signal_study",
|
|
123
|
+
"idea_rule_simulation",
|
|
124
|
+
"variant_sensitivity"
|
|
125
|
+
]),
|
|
126
|
+
supported: z.boolean(),
|
|
127
|
+
unsupportedReason: z.string().optional(),
|
|
128
|
+
summary: z.string(),
|
|
129
|
+
dataLineage: z.object({
|
|
130
|
+
venue: z.string(),
|
|
131
|
+
symbols: z.array(z.string()),
|
|
132
|
+
resolution: quantResolutionSchema,
|
|
133
|
+
timeframeStart: z.string(),
|
|
134
|
+
timeframeEnd: z.string(),
|
|
135
|
+
sourceIds: z.array(z.string()),
|
|
136
|
+
warnings: z.array(z.string())
|
|
137
|
+
}).strict(),
|
|
138
|
+
signalStudy: z.record(z.string(), z.unknown()).optional(),
|
|
139
|
+
ideaSimulation: z.record(z.string(), z.unknown()).optional(),
|
|
140
|
+
decisionArtifact: quantDecisionArtifactV1Schema.optional(),
|
|
141
|
+
warnings: z.array(z.string()).default([])
|
|
142
|
+
}).strict();
|
|
143
|
+
|
|
144
|
+
// src/quant/bars.ts
|
|
145
|
+
function normalizeQuantBars(input) {
|
|
146
|
+
const bars = quantBarSchema.array().parse(input).slice();
|
|
147
|
+
bars.sort((a, b) => a.time - b.time);
|
|
148
|
+
for (let index = 0; index < bars.length; index += 1) {
|
|
149
|
+
const bar = bars[index];
|
|
150
|
+
if (bar.high < Math.max(bar.open, bar.close) || bar.low > Math.min(bar.open, bar.close)) {
|
|
151
|
+
throw new Error(`Invalid OHLC relationship at bar ${index}`);
|
|
152
|
+
}
|
|
153
|
+
if (index > 0 && bar.time <= bars[index - 1].time) {
|
|
154
|
+
throw new Error(`Duplicate or non-increasing bar time at index ${index}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return bars;
|
|
158
|
+
}
|
|
159
|
+
function closePrices(bars) {
|
|
160
|
+
return bars.map((bar) => bar.close);
|
|
161
|
+
}
|
|
162
|
+
function typicalPrices(bars) {
|
|
163
|
+
return bars.map((bar) => (bar.high + bar.low + bar.close) / 3);
|
|
164
|
+
}
|
|
165
|
+
function sliceBarsToWindow(params) {
|
|
166
|
+
const startIndex = params.bars.findIndex((bar) => bar.time >= params.startSeconds);
|
|
167
|
+
const fromIndex = startIndex < 0 ? params.bars.length : Math.max(0, startIndex - (params.warmupBars ?? 0));
|
|
168
|
+
return params.bars.slice(fromIndex).filter((bar) => bar.time <= params.endSeconds);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/quant/decision-series.ts
|
|
172
|
+
function validateDecisionArtifact(value) {
|
|
173
|
+
const artifact = quantDecisionArtifactV1Schema.parse(value);
|
|
174
|
+
for (let index = 1; index < artifact.decisions.length; index += 1) {
|
|
175
|
+
if (artifact.decisions[index].time < artifact.decisions[index - 1].time) {
|
|
176
|
+
throw new Error(`Decision artifact times must be sorted at index ${index}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return artifact;
|
|
180
|
+
}
|
|
181
|
+
function compactDecisionChanges(decisions) {
|
|
182
|
+
const compact = [];
|
|
183
|
+
let previousTarget = null;
|
|
184
|
+
for (const decision of decisions) {
|
|
185
|
+
if (previousTarget === null || decision.targetPosition !== previousTarget) {
|
|
186
|
+
compact.push(decision);
|
|
187
|
+
previousTarget = decision.targetPosition;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return compact;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/quant/features/beta.ts
|
|
194
|
+
function beta(assetReturns, benchmarkReturns) {
|
|
195
|
+
const length = Math.min(assetReturns.length, benchmarkReturns.length);
|
|
196
|
+
if (length < 2) return null;
|
|
197
|
+
const asset = assetReturns.slice(assetReturns.length - length);
|
|
198
|
+
const benchmark = benchmarkReturns.slice(benchmarkReturns.length - length);
|
|
199
|
+
const meanAsset = asset.reduce((total, value) => total + value, 0) / length;
|
|
200
|
+
const meanBenchmark = benchmark.reduce((total, value) => total + value, 0) / length;
|
|
201
|
+
let covariance = 0;
|
|
202
|
+
let benchmarkVariance = 0;
|
|
203
|
+
for (let index = 0; index < length; index += 1) {
|
|
204
|
+
covariance += (asset[index] - meanAsset) * (benchmark[index] - meanBenchmark);
|
|
205
|
+
benchmarkVariance += (benchmark[index] - meanBenchmark) ** 2;
|
|
206
|
+
}
|
|
207
|
+
return benchmarkVariance === 0 ? null : covariance / benchmarkVariance;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/quant/features/correlation.ts
|
|
211
|
+
function correlation(a, b) {
|
|
212
|
+
const length = Math.min(a.length, b.length);
|
|
213
|
+
if (length < 2) return null;
|
|
214
|
+
const left = a.slice(a.length - length);
|
|
215
|
+
const right = b.slice(b.length - length);
|
|
216
|
+
const meanA = left.reduce((total, value) => total + value, 0) / length;
|
|
217
|
+
const meanB = right.reduce((total, value) => total + value, 0) / length;
|
|
218
|
+
let covariance = 0;
|
|
219
|
+
let varianceA = 0;
|
|
220
|
+
let varianceB = 0;
|
|
221
|
+
for (let index = 0; index < length; index += 1) {
|
|
222
|
+
const da = left[index] - meanA;
|
|
223
|
+
const db = right[index] - meanB;
|
|
224
|
+
covariance += da * db;
|
|
225
|
+
varianceA += da * da;
|
|
226
|
+
varianceB += db * db;
|
|
227
|
+
}
|
|
228
|
+
if (varianceA === 0 || varianceB === 0) return null;
|
|
229
|
+
return covariance / Math.sqrt(varianceA * varianceB);
|
|
230
|
+
}
|
|
231
|
+
function rollingCorrelation(a, b, period = 20) {
|
|
232
|
+
return a.map((_, index) => {
|
|
233
|
+
if (index < period - 1) return null;
|
|
234
|
+
return correlation(
|
|
235
|
+
a.slice(index - period + 1, index + 1),
|
|
236
|
+
b.slice(index - period + 1, index + 1)
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/quant/features/funding.ts
|
|
242
|
+
function fundingRates(bars) {
|
|
243
|
+
return bars.map((bar) => bar.fundingRate ?? 0);
|
|
244
|
+
}
|
|
245
|
+
function cumulativeFunding(bars) {
|
|
246
|
+
let running = 0;
|
|
247
|
+
return fundingRates(bars).map((rate) => {
|
|
248
|
+
running += rate;
|
|
249
|
+
return running;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/quant/features/momentum.ts
|
|
254
|
+
function momentum(values, lookbackBars) {
|
|
255
|
+
if (!Number.isInteger(lookbackBars) || lookbackBars <= 0) {
|
|
256
|
+
throw new Error("Momentum lookback must be a positive integer");
|
|
257
|
+
}
|
|
258
|
+
return values.map((value, index) => {
|
|
259
|
+
const prior = values[index - lookbackBars];
|
|
260
|
+
return prior == null || prior === 0 ? null : value / prior - 1;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/quant/features/relative-strength.ts
|
|
265
|
+
function relativeStrength(assetPrices, benchmarkPrices, lookbackBars) {
|
|
266
|
+
const assetMomentum = momentum(assetPrices, lookbackBars);
|
|
267
|
+
const benchmarkMomentum = momentum(benchmarkPrices, lookbackBars);
|
|
268
|
+
return assetMomentum.map(
|
|
269
|
+
(value, index) => value == null || benchmarkMomentum[index] == null ? null : value - benchmarkMomentum[index]
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/quant/features/returns.ts
|
|
274
|
+
function simpleReturns(values) {
|
|
275
|
+
return values.map((value, index) => {
|
|
276
|
+
if (index === 0) return 0;
|
|
277
|
+
const previous = values[index - 1];
|
|
278
|
+
return previous === 0 ? 0 : value / previous - 1;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function logReturns(values) {
|
|
282
|
+
return values.map((value, index) => {
|
|
283
|
+
if (index === 0) return 0;
|
|
284
|
+
const previous = values[index - 1];
|
|
285
|
+
return previous <= 0 || value <= 0 ? 0 : Math.log(value / previous);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function forwardReturns(values, horizonBars2) {
|
|
289
|
+
if (!Number.isInteger(horizonBars2) || horizonBars2 <= 0) {
|
|
290
|
+
throw new Error("Forward return horizon must be a positive integer");
|
|
291
|
+
}
|
|
292
|
+
return values.map((value, index) => {
|
|
293
|
+
const future = values[index + horizonBars2];
|
|
294
|
+
return future == null || value === 0 ? null : future / value - 1;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/quant/indicators/sma.ts
|
|
299
|
+
function sma(values, period) {
|
|
300
|
+
if (!Number.isInteger(period) || period <= 0) {
|
|
301
|
+
throw new Error("SMA period must be a positive integer");
|
|
302
|
+
}
|
|
303
|
+
const output = [];
|
|
304
|
+
let sum = 0;
|
|
305
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
306
|
+
sum += values[index];
|
|
307
|
+
if (index >= period) sum -= values[index - period];
|
|
308
|
+
output.push(index >= period - 1 ? sum / period : null);
|
|
309
|
+
}
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/quant/features/volume.ts
|
|
314
|
+
function volumes(bars) {
|
|
315
|
+
return bars.map((bar) => bar.volume ?? 0);
|
|
316
|
+
}
|
|
317
|
+
function relativeVolume(bars, period = 20) {
|
|
318
|
+
const raw = volumes(bars);
|
|
319
|
+
const average = sma(raw, period);
|
|
320
|
+
return raw.map((value, index) => {
|
|
321
|
+
const baseline = average[index];
|
|
322
|
+
return baseline == null || baseline === 0 ? null : value / baseline;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/quant/indicators/atr.ts
|
|
327
|
+
function trueRanges(bars) {
|
|
328
|
+
return bars.map((bar, index) => {
|
|
329
|
+
const previousClose = index > 0 ? bars[index - 1].close : bar.close;
|
|
330
|
+
return Math.max(
|
|
331
|
+
bar.high - bar.low,
|
|
332
|
+
Math.abs(bar.high - previousClose),
|
|
333
|
+
Math.abs(bar.low - previousClose)
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function atr(bars, period = 14) {
|
|
338
|
+
return sma(trueRanges(bars), period);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/quant/indicators/bollinger.ts
|
|
342
|
+
function bollinger(values, period = 20, standardDeviations = 2) {
|
|
343
|
+
const middle = sma(values, period);
|
|
344
|
+
return values.map((_, index) => {
|
|
345
|
+
const average = middle[index];
|
|
346
|
+
if (average == null || index < period - 1) {
|
|
347
|
+
return { lower: null, middle: null, upper: null, width: null };
|
|
348
|
+
}
|
|
349
|
+
const window = values.slice(index - period + 1, index + 1);
|
|
350
|
+
const variance = window.reduce((total, value) => total + (value - average) ** 2, 0) / period;
|
|
351
|
+
const deviation = Math.sqrt(variance);
|
|
352
|
+
const lower = average - deviation * standardDeviations;
|
|
353
|
+
const upper = average + deviation * standardDeviations;
|
|
354
|
+
return {
|
|
355
|
+
lower,
|
|
356
|
+
middle: average,
|
|
357
|
+
upper,
|
|
358
|
+
width: average === 0 ? null : (upper - lower) / average
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/quant/indicators/donchian.ts
|
|
364
|
+
function donchian(highs, lows, period = 20) {
|
|
365
|
+
if (highs.length !== lows.length) {
|
|
366
|
+
throw new Error("Donchian high/low arrays must have the same length");
|
|
367
|
+
}
|
|
368
|
+
if (!Number.isInteger(period) || period <= 0) {
|
|
369
|
+
throw new Error("Donchian period must be a positive integer");
|
|
370
|
+
}
|
|
371
|
+
return highs.map((_, index) => {
|
|
372
|
+
if (index < period - 1) return { lower: null, upper: null };
|
|
373
|
+
const highWindow = highs.slice(index - period + 1, index + 1);
|
|
374
|
+
const lowWindow = lows.slice(index - period + 1, index + 1);
|
|
375
|
+
return {
|
|
376
|
+
lower: Math.min(...lowWindow),
|
|
377
|
+
upper: Math.max(...highWindow)
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/quant/indicators/ema.ts
|
|
383
|
+
function ema(values, period) {
|
|
384
|
+
if (!Number.isInteger(period) || period <= 0) {
|
|
385
|
+
throw new Error("EMA period must be a positive integer");
|
|
386
|
+
}
|
|
387
|
+
const output = [];
|
|
388
|
+
const multiplier = 2 / (period + 1);
|
|
389
|
+
let previous = null;
|
|
390
|
+
let seedSum = 0;
|
|
391
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
392
|
+
const value = values[index];
|
|
393
|
+
if (index < period) {
|
|
394
|
+
seedSum += value;
|
|
395
|
+
if (index === period - 1) {
|
|
396
|
+
previous = seedSum / period;
|
|
397
|
+
output.push(previous);
|
|
398
|
+
} else {
|
|
399
|
+
output.push(null);
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
previous = previous == null ? value : (value - previous) * multiplier + previous;
|
|
404
|
+
output.push(previous);
|
|
405
|
+
}
|
|
406
|
+
return output;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/quant/indicators/macd.ts
|
|
410
|
+
function macd(values, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
|
|
411
|
+
if (fastPeriod >= slowPeriod) {
|
|
412
|
+
throw new Error("MACD fast period must be less than slow period");
|
|
413
|
+
}
|
|
414
|
+
const fast = ema(values, fastPeriod);
|
|
415
|
+
const slow = ema(values, slowPeriod);
|
|
416
|
+
const macdLine = values.map(
|
|
417
|
+
(_, index) => fast[index] == null || slow[index] == null ? null : fast[index] - slow[index]
|
|
418
|
+
);
|
|
419
|
+
const signalInput = macdLine.map((value) => value ?? 0);
|
|
420
|
+
const signalLine = ema(signalInput, signalPeriod);
|
|
421
|
+
return values.map((_, index) => {
|
|
422
|
+
const macdValue = macdLine[index];
|
|
423
|
+
const signalValue = macdValue == null ? null : signalLine[index];
|
|
424
|
+
return {
|
|
425
|
+
macd: macdValue,
|
|
426
|
+
signal: signalValue,
|
|
427
|
+
histogram: macdValue == null || signalValue == null ? null : macdValue - signalValue
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/quant/indicators/rsi.ts
|
|
433
|
+
function rsi(values, period = 14) {
|
|
434
|
+
if (!Number.isInteger(period) || period <= 0) {
|
|
435
|
+
throw new Error("RSI period must be a positive integer");
|
|
436
|
+
}
|
|
437
|
+
const output = Array.from({ length: values.length }, () => null);
|
|
438
|
+
if (values.length <= period) return output;
|
|
439
|
+
let gainSum = 0;
|
|
440
|
+
let lossSum = 0;
|
|
441
|
+
for (let index = 1; index <= period; index += 1) {
|
|
442
|
+
const delta = values[index] - values[index - 1];
|
|
443
|
+
if (delta >= 0) gainSum += delta;
|
|
444
|
+
else lossSum += Math.abs(delta);
|
|
445
|
+
}
|
|
446
|
+
let averageGain = gainSum / period;
|
|
447
|
+
let averageLoss = lossSum / period;
|
|
448
|
+
output[period] = averageLoss === 0 ? 100 : 100 - 100 / (1 + averageGain / averageLoss);
|
|
449
|
+
for (let index = period + 1; index < values.length; index += 1) {
|
|
450
|
+
const delta = values[index] - values[index - 1];
|
|
451
|
+
const gain = delta > 0 ? delta : 0;
|
|
452
|
+
const loss = delta < 0 ? Math.abs(delta) : 0;
|
|
453
|
+
averageGain = (averageGain * (period - 1) + gain) / period;
|
|
454
|
+
averageLoss = (averageLoss * (period - 1) + loss) / period;
|
|
455
|
+
output[index] = averageLoss === 0 ? 100 : 100 - 100 / (1 + averageGain / averageLoss);
|
|
456
|
+
}
|
|
457
|
+
return output;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/quant/indicators/volatility.ts
|
|
461
|
+
function rollingVolatility(returns, period = 20, annualization = 365) {
|
|
462
|
+
if (!Number.isInteger(period) || period <= 1) {
|
|
463
|
+
throw new Error("Volatility period must be an integer greater than 1");
|
|
464
|
+
}
|
|
465
|
+
return returns.map((_, index) => {
|
|
466
|
+
if (index < period - 1) return null;
|
|
467
|
+
const window = returns.slice(index - period + 1, index + 1);
|
|
468
|
+
const mean = window.reduce((total, value) => total + value, 0) / period;
|
|
469
|
+
const variance = window.reduce((total, value) => total + (value - mean) ** 2, 0) / (period - 1);
|
|
470
|
+
return Math.sqrt(variance) * Math.sqrt(annualization);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/quant/indicators/zscore.ts
|
|
475
|
+
function rollingZScore(values, period = 20) {
|
|
476
|
+
if (!Number.isInteger(period) || period <= 1) {
|
|
477
|
+
throw new Error("Z-score period must be an integer greater than 1");
|
|
478
|
+
}
|
|
479
|
+
return values.map((value, index) => {
|
|
480
|
+
if (index < period - 1) return null;
|
|
481
|
+
const window = values.slice(index - period + 1, index + 1);
|
|
482
|
+
const mean = window.reduce((total, current) => total + current, 0) / period;
|
|
483
|
+
const variance = window.reduce((total, current) => total + (current - mean) ** 2, 0) / period;
|
|
484
|
+
const deviation = Math.sqrt(variance);
|
|
485
|
+
return deviation === 0 ? 0 : (value - mean) / deviation;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/quant/lineage.ts
|
|
490
|
+
function buildQuantDataLineage(params) {
|
|
491
|
+
const symbol = params.request.idea.market.symbol ?? params.request.idea.market.universe?.[0] ?? "UNKNOWN";
|
|
492
|
+
return {
|
|
493
|
+
venue: params.request.idea.market.venue,
|
|
494
|
+
symbols: params.request.idea.market.universe ?? [symbol],
|
|
495
|
+
resolution: params.request.window.resolution,
|
|
496
|
+
timeframeStart: params.request.window.timeframeStart,
|
|
497
|
+
timeframeEnd: params.request.window.timeframeEnd,
|
|
498
|
+
sourceIds: params.request.idea.requiredSources,
|
|
499
|
+
warnings: params.warnings ?? []
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/quant/result.ts
|
|
504
|
+
function summarizeNumbers(values) {
|
|
505
|
+
const finite = values.filter((value) => Number.isFinite(value));
|
|
506
|
+
if (finite.length === 0) {
|
|
507
|
+
return {
|
|
508
|
+
count: 0,
|
|
509
|
+
max: null,
|
|
510
|
+
mean: null,
|
|
511
|
+
median: null,
|
|
512
|
+
min: null,
|
|
513
|
+
positiveRate: null,
|
|
514
|
+
standardDeviation: null
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const sorted = finite.slice().sort((a, b) => a - b);
|
|
518
|
+
const mean = finite.reduce((total, value) => total + value, 0) / finite.length;
|
|
519
|
+
const variance = finite.reduce((total, value) => total + (value - mean) ** 2, 0) / finite.length;
|
|
520
|
+
const middle = Math.floor(sorted.length / 2);
|
|
521
|
+
const median = sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
522
|
+
return {
|
|
523
|
+
count: finite.length,
|
|
524
|
+
max: sorted[sorted.length - 1],
|
|
525
|
+
mean,
|
|
526
|
+
median,
|
|
527
|
+
min: sorted[0],
|
|
528
|
+
positiveRate: finite.filter((value) => value > 0).length / Math.max(1, finite.length),
|
|
529
|
+
standardDeviation: Math.sqrt(variance)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/quant/signal-study/adverse-excursion.ts
|
|
534
|
+
function summarizeExcursions(params) {
|
|
535
|
+
const adverse = [];
|
|
536
|
+
const favorable = [];
|
|
537
|
+
for (let index = 0; index < params.bars.length; index += 1) {
|
|
538
|
+
if (!params.condition[index]) continue;
|
|
539
|
+
const entry = params.bars[index].close;
|
|
540
|
+
const window = params.bars.slice(index + 1, index + params.horizonBars + 1);
|
|
541
|
+
if (window.length === 0) continue;
|
|
542
|
+
adverse.push(Math.min(...window.map((bar) => bar.low / entry - 1)));
|
|
543
|
+
favorable.push(Math.max(...window.map((bar) => bar.high / entry - 1)));
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
averageAdverse: adverse.length === 0 ? null : adverse.reduce((total, value) => total + value, 0) / adverse.length,
|
|
547
|
+
averageFavorable: favorable.length === 0 ? null : favorable.reduce((total, value) => total + value, 0) / favorable.length,
|
|
548
|
+
count: adverse.length
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/quant/signal-study/event-windows.ts
|
|
553
|
+
function buildEventWindows(params) {
|
|
554
|
+
const windows = [];
|
|
555
|
+
for (let index = 0; index < params.bars.length; index += 1) {
|
|
556
|
+
if (!params.condition[index]) continue;
|
|
557
|
+
const start = params.bars[Math.max(0, index - params.preBars)];
|
|
558
|
+
const end = params.bars[Math.min(params.bars.length - 1, index + params.postBars)];
|
|
559
|
+
windows.push({
|
|
560
|
+
endTime: end.time,
|
|
561
|
+
eventTime: params.bars[index].time,
|
|
562
|
+
startTime: start.time
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return windows;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/quant/signal-study/forward-returns.ts
|
|
569
|
+
function studyForwardReturns(params) {
|
|
570
|
+
const forward = forwardReturns(params.prices, params.horizonBars);
|
|
571
|
+
const all = forward.filter((value) => value != null);
|
|
572
|
+
const conditioned = forward.filter(
|
|
573
|
+
(value, index) => value != null && params.condition[index] === true
|
|
574
|
+
);
|
|
575
|
+
return {
|
|
576
|
+
conditioned: summarizeNumbers(conditioned),
|
|
577
|
+
horizonBars: params.horizonBars,
|
|
578
|
+
unconditional: summarizeNumbers(all)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/quant/signal-study/hit-rate.ts
|
|
583
|
+
function hitRate(values) {
|
|
584
|
+
const finite = values.filter((value) => Number.isFinite(value));
|
|
585
|
+
if (finite.length === 0) return null;
|
|
586
|
+
return finite.filter((value) => value > 0).length / finite.length;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/quant/signal-study/information-coefficient.ts
|
|
590
|
+
function informationCoefficient(params) {
|
|
591
|
+
const pairs = [];
|
|
592
|
+
const length = Math.min(params.signal.length, params.forwardReturns.length);
|
|
593
|
+
for (let index = 0; index < length; index += 1) {
|
|
594
|
+
const x = params.signal[index];
|
|
595
|
+
const y = params.forwardReturns[index];
|
|
596
|
+
if (x != null && y != null && Number.isFinite(x) && Number.isFinite(y)) {
|
|
597
|
+
pairs.push({ x, y });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (pairs.length < 3) return null;
|
|
601
|
+
const meanX = pairs.reduce((total, pair) => total + pair.x, 0) / pairs.length;
|
|
602
|
+
const meanY = pairs.reduce((total, pair) => total + pair.y, 0) / pairs.length;
|
|
603
|
+
let covariance = 0;
|
|
604
|
+
let varianceX = 0;
|
|
605
|
+
let varianceY = 0;
|
|
606
|
+
for (const pair of pairs) {
|
|
607
|
+
const dx = pair.x - meanX;
|
|
608
|
+
const dy = pair.y - meanY;
|
|
609
|
+
covariance += dx * dy;
|
|
610
|
+
varianceX += dx * dx;
|
|
611
|
+
varianceY += dy * dy;
|
|
612
|
+
}
|
|
613
|
+
if (varianceX === 0 || varianceY === 0) return null;
|
|
614
|
+
return covariance / Math.sqrt(varianceX * varianceY);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/quant/signal-study/summary.ts
|
|
618
|
+
function signalStudySummary(params) {
|
|
619
|
+
if (params.conditionedCount === 0) {
|
|
620
|
+
return "No signal events were available in the supplied window.";
|
|
621
|
+
}
|
|
622
|
+
const conditioned = params.conditionedMean == null ? "n/a" : `${(params.conditionedMean * 100).toFixed(2)}%`;
|
|
623
|
+
const unconditional = params.unconditionalMean == null ? "n/a" : `${(params.unconditionalMean * 100).toFixed(2)}%`;
|
|
624
|
+
return `Signal events=${params.conditionedCount}; conditioned forward return=${conditioned}; unconditional=${unconditional}.`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/quant/strategies/breakout.ts
|
|
628
|
+
var DONCHIAN_BREAKOUT_RULE_KIND = "donchian_breakout";
|
|
629
|
+
|
|
630
|
+
// src/quant/strategies/funding-carry.ts
|
|
631
|
+
var FUNDING_CARRY_RULE_KIND = "funding_carry";
|
|
632
|
+
|
|
633
|
+
// src/quant/strategies/macd-trend.ts
|
|
634
|
+
var MACD_CROSSOVER_RULE_KIND = "macd_crossover";
|
|
635
|
+
|
|
636
|
+
// src/quant/strategies/ma-cross.ts
|
|
637
|
+
var MA_CROSS_RULE_KIND = "ma_cross";
|
|
638
|
+
|
|
639
|
+
// src/quant/strategies/momentum.ts
|
|
640
|
+
var MOMENTUM_RULE_KIND = "momentum";
|
|
641
|
+
|
|
642
|
+
// src/quant/strategies/registry.ts
|
|
643
|
+
var BASE_TESTS = [
|
|
644
|
+
"prompt_plan_check",
|
|
645
|
+
"data_availability",
|
|
646
|
+
"signal_study",
|
|
647
|
+
"idea_rule_simulation",
|
|
648
|
+
"variant_sensitivity",
|
|
649
|
+
"leakage_check",
|
|
650
|
+
"cost_stress"
|
|
651
|
+
];
|
|
652
|
+
var QUANT_FAMILY_CAPABILITIES = [
|
|
653
|
+
{
|
|
654
|
+
aliases: ["benchmark", "buy-and-hold", "buy_and_hold", "hold"],
|
|
655
|
+
family: "benchmark",
|
|
656
|
+
supportLevel: "v1_replayable",
|
|
657
|
+
supportedTestKinds: BASE_TESTS
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
aliases: ["signal_rule", "moving_average", "ma_cross", "rsi", "macd", "bollinger", "donchian"],
|
|
661
|
+
family: "signal_rule",
|
|
662
|
+
supportLevel: "v1_replayable",
|
|
663
|
+
supportedTestKinds: BASE_TESTS
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
aliases: ["trend", "momentum", "breakout", "time_series_momentum"],
|
|
667
|
+
family: "trend",
|
|
668
|
+
supportLevel: "v1_replayable",
|
|
669
|
+
supportedTestKinds: BASE_TESTS
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
aliases: ["mean_reversion", "zscore", "rsi_mean_reversion", "bollinger_mean_reversion"],
|
|
673
|
+
family: "mean_reversion",
|
|
674
|
+
supportLevel: "v1_replayable",
|
|
675
|
+
supportedTestKinds: BASE_TESTS
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
aliases: ["stat_arb", "pairs", "pair_spread", "cointegration"],
|
|
679
|
+
family: "stat_arb",
|
|
680
|
+
supportLevel: "v1_signal_or_idea_test",
|
|
681
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability", "signal_study", "leakage_check"]
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
aliases: ["carry", "funding", "basis", "funding_carry"],
|
|
685
|
+
family: "carry",
|
|
686
|
+
supportLevel: "v1_signal_or_idea_test",
|
|
687
|
+
supportedTestKinds: BASE_TESTS
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
aliases: ["hedge", "overlay", "tail_hedge"],
|
|
691
|
+
family: "hedge",
|
|
692
|
+
supportLevel: "v1_signal_or_idea_test",
|
|
693
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability", "signal_study", "cost_stress"]
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
aliases: ["execution", "twap", "vwap", "participation", "iceberg"],
|
|
697
|
+
family: "execution",
|
|
698
|
+
supportLevel: "advanced_research",
|
|
699
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability", "cost_stress"]
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
aliases: ["market_making", "maker", "avellaneda", "inventory_skew"],
|
|
703
|
+
family: "market_making",
|
|
704
|
+
supportLevel: "advanced_research",
|
|
705
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability", "cost_stress"]
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
aliases: ["options", "vol", "black_scholes", "put_call_parity"],
|
|
709
|
+
family: "options",
|
|
710
|
+
supportLevel: "unsupported_until_data",
|
|
711
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability"]
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
aliases: ["regime_ml", "ml", "regime", "markov", "hmm", "rl"],
|
|
715
|
+
family: "regime_ml",
|
|
716
|
+
supportLevel: "advanced_research",
|
|
717
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability", "leakage_check"]
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
aliases: ["prediction_market", "outcome", "hip4", "binary"],
|
|
721
|
+
family: "prediction_market",
|
|
722
|
+
supportLevel: "v1_signal_or_idea_test",
|
|
723
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability", "signal_study", "cost_stress"]
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
aliases: ["defi_lp", "amm", "lp", "concentrated_liquidity"],
|
|
727
|
+
family: "defi_lp",
|
|
728
|
+
supportLevel: "unsupported_until_data",
|
|
729
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability"]
|
|
730
|
+
}
|
|
731
|
+
];
|
|
732
|
+
function resolveQuantFamilyCapability(value) {
|
|
733
|
+
const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
734
|
+
return QUANT_FAMILY_CAPABILITIES.find(
|
|
735
|
+
(capability) => capability.family === normalized || capability.aliases.some((alias) => alias === normalized)
|
|
736
|
+
) ?? null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/quant/strategies/rsi-mean-reversion.ts
|
|
740
|
+
var RSI_MEAN_REVERSION_RULE_KIND = "rsi_mean_reversion";
|
|
741
|
+
|
|
742
|
+
// src/quant/strategies/rule-evaluator.ts
|
|
743
|
+
function numberParam(params, key, fallback) {
|
|
744
|
+
const value = params[key];
|
|
745
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
746
|
+
}
|
|
747
|
+
function stringParam(params, key, fallback) {
|
|
748
|
+
const value = params[key];
|
|
749
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
750
|
+
}
|
|
751
|
+
function ma(values, type, period) {
|
|
752
|
+
return type.toLowerCase() === "sma" ? sma(values, period) : ema(values, period);
|
|
753
|
+
}
|
|
754
|
+
function shiftedBreakoutCondition(bars, period) {
|
|
755
|
+
const channels = donchian(
|
|
756
|
+
bars.map((bar) => bar.high),
|
|
757
|
+
bars.map((bar) => bar.low),
|
|
758
|
+
period
|
|
759
|
+
);
|
|
760
|
+
return bars.map((bar, index) => {
|
|
761
|
+
const prior = channels[index - 1];
|
|
762
|
+
return prior?.upper != null && bar.close > prior.upper;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function evaluateQuantRule(params) {
|
|
766
|
+
const prices = closePrices(params.bars);
|
|
767
|
+
const rule = params.idea.rule;
|
|
768
|
+
const kind = rule?.kind ?? (params.idea.family === "benchmark" ? "buy_hold" : "momentum");
|
|
769
|
+
const ruleParams = rule?.params ?? {};
|
|
770
|
+
const warnings = [];
|
|
771
|
+
let condition = [];
|
|
772
|
+
let positions = [];
|
|
773
|
+
let signal = [];
|
|
774
|
+
if (kind === "buy_hold") {
|
|
775
|
+
condition = prices.map(() => true);
|
|
776
|
+
positions = prices.map(() => 1);
|
|
777
|
+
signal = prices.map(() => 1);
|
|
778
|
+
} else if (kind === "ma_cross" || kind === "moving_average_crossover") {
|
|
779
|
+
const fastPeriod = Math.max(1, Math.trunc(numberParam(ruleParams, "fastPeriod", 20)));
|
|
780
|
+
const slowPeriod = Math.max(fastPeriod + 1, Math.trunc(numberParam(ruleParams, "slowPeriod", 100)));
|
|
781
|
+
const averageType = stringParam(ruleParams, "averageType", "ema");
|
|
782
|
+
const fast = ma(prices, averageType, fastPeriod);
|
|
783
|
+
const slow = ma(prices, averageType, slowPeriod);
|
|
784
|
+
signal = prices.map(
|
|
785
|
+
(_, index) => fast[index] == null || slow[index] == null ? null : fast[index] - slow[index]
|
|
786
|
+
);
|
|
787
|
+
condition = signal.map((value) => value != null && value > 0);
|
|
788
|
+
positions = condition.map((active) => active ? 1 : 0);
|
|
789
|
+
} else if (kind === "rsi_mean_reversion") {
|
|
790
|
+
const period = Math.max(2, Math.trunc(numberParam(ruleParams, "period", 14)));
|
|
791
|
+
const oversold = numberParam(ruleParams, "oversold", 30);
|
|
792
|
+
const exit = numberParam(ruleParams, "exit", 50);
|
|
793
|
+
signal = rsi(prices, period);
|
|
794
|
+
let active = false;
|
|
795
|
+
positions = signal.map((value) => {
|
|
796
|
+
if (value == null) return 0;
|
|
797
|
+
if (value <= oversold) active = true;
|
|
798
|
+
if (value >= exit) active = false;
|
|
799
|
+
return active ? 1 : 0;
|
|
800
|
+
});
|
|
801
|
+
condition = signal.map((value) => value != null && value <= oversold);
|
|
802
|
+
} else if (kind === "macd_crossover") {
|
|
803
|
+
const points = macd(prices);
|
|
804
|
+
signal = points.map(
|
|
805
|
+
(point) => point.macd == null || point.signal == null ? null : point.macd - point.signal
|
|
806
|
+
);
|
|
807
|
+
condition = signal.map((value) => value != null && value > 0);
|
|
808
|
+
positions = condition.map((active) => active ? 1 : 0);
|
|
809
|
+
} else if (kind === "donchian_breakout") {
|
|
810
|
+
const period = Math.max(2, Math.trunc(numberParam(ruleParams, "period", 20)));
|
|
811
|
+
condition = shiftedBreakoutCondition(params.bars, period);
|
|
812
|
+
signal = condition.map((active) => active ? 1 : 0);
|
|
813
|
+
positions = condition.map((active) => active ? 1 : 0);
|
|
814
|
+
} else if (kind === "zscore_mean_reversion") {
|
|
815
|
+
const period = Math.max(2, Math.trunc(numberParam(ruleParams, "period", 20)));
|
|
816
|
+
const entry = Math.abs(numberParam(ruleParams, "entry", 2));
|
|
817
|
+
const exit = Math.abs(numberParam(ruleParams, "exit", 0.25));
|
|
818
|
+
signal = rollingZScore(prices, period);
|
|
819
|
+
let active = false;
|
|
820
|
+
positions = signal.map((value) => {
|
|
821
|
+
if (value == null) return 0;
|
|
822
|
+
if (value <= -entry) active = true;
|
|
823
|
+
if (Math.abs(value) <= exit || value >= entry) active = false;
|
|
824
|
+
return active ? 1 : 0;
|
|
825
|
+
});
|
|
826
|
+
condition = signal.map((value) => value != null && value <= -entry);
|
|
827
|
+
} else if (kind === "momentum") {
|
|
828
|
+
const lookbackBars = Math.max(1, Math.trunc(numberParam(ruleParams, "lookbackBars", 30)));
|
|
829
|
+
const threshold = numberParam(ruleParams, "threshold", 0);
|
|
830
|
+
signal = momentum(prices, lookbackBars);
|
|
831
|
+
condition = signal.map((value) => value != null && value > threshold);
|
|
832
|
+
positions = condition.map((active) => active ? 1 : 0);
|
|
833
|
+
} else if (kind === "funding_carry") {
|
|
834
|
+
const threshold = numberParam(ruleParams, "maxFundingRate", 0);
|
|
835
|
+
signal = fundingRates(params.bars);
|
|
836
|
+
condition = signal.map((value) => value != null && value <= threshold);
|
|
837
|
+
positions = condition.map((active) => active ? 1 : 0);
|
|
838
|
+
} else {
|
|
839
|
+
warnings.push(`Rule kind ${kind} is not supported by the V1 evaluator; using signal-only flat positions.`);
|
|
840
|
+
condition = prices.map(() => false);
|
|
841
|
+
positions = prices.map(() => 0);
|
|
842
|
+
signal = prices.map(() => null);
|
|
843
|
+
}
|
|
844
|
+
return { condition, positions, signal, warnings };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/quant/strategies/zscore-mean-reversion.ts
|
|
848
|
+
var ZSCORE_MEAN_REVERSION_RULE_KIND = "zscore_mean_reversion";
|
|
849
|
+
|
|
850
|
+
// src/quant/tester/plan.ts
|
|
851
|
+
function buildQuantTestPlan(idea) {
|
|
852
|
+
const capability = resolveQuantFamilyCapability(idea.family);
|
|
853
|
+
if (!capability) {
|
|
854
|
+
return {
|
|
855
|
+
supportedTestKinds: ["prompt_plan_check", "data_availability"],
|
|
856
|
+
supportLevel: "unsupported_until_data",
|
|
857
|
+
warnings: [`No V1 quant capability found for family ${idea.family}.`]
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
supportedTestKinds: capability.supportedTestKinds,
|
|
862
|
+
supportLevel: capability.supportLevel,
|
|
863
|
+
warnings: capability.supportLevel === "advanced_research" || capability.supportLevel === "unsupported_until_data" ? [`Family ${idea.family} is ${capability.supportLevel}; do not mark it strict-backtest-ready.`] : []
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/quant/tester/report.ts
|
|
868
|
+
function finalizeQuantTesterReport(report) {
|
|
869
|
+
return quantTesterReportV1Schema.parse(report);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/quant/tester/warnings.ts
|
|
873
|
+
function quantDataWarnings(params) {
|
|
874
|
+
const warnings = [];
|
|
875
|
+
if (params.bars.length < 50) {
|
|
876
|
+
warnings.push("Sample has fewer than 50 bars; treat metrics as unstable.");
|
|
877
|
+
}
|
|
878
|
+
if (params.bars.length > 1) {
|
|
879
|
+
const gaps = [];
|
|
880
|
+
for (let index = 1; index < params.bars.length; index += 1) {
|
|
881
|
+
gaps.push(params.bars[index].time - params.bars[index - 1].time);
|
|
882
|
+
}
|
|
883
|
+
const medianGap = gaps.slice().sort((a, b) => a - b)[Math.floor(gaps.length / 2)];
|
|
884
|
+
if (gaps.some((gap) => gap > medianGap * 2)) {
|
|
885
|
+
warnings.push("Bars contain time gaps larger than twice the median interval.");
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (params.request.variantSpace && Object.keys(params.request.variantSpace).length > 20) {
|
|
889
|
+
warnings.push("Variant space is broad; use multiple-comparison controls before promotion.");
|
|
890
|
+
}
|
|
891
|
+
return warnings;
|
|
892
|
+
}
|
|
893
|
+
function quantCostBps(request) {
|
|
894
|
+
return (request.assumptions.takerFeeBps ?? request.assumptions.makerFeeBps ?? 0) + (request.assumptions.slippageBps ?? 0);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/quant/tester/run-idea-test.ts
|
|
898
|
+
function symbolForRequest(request) {
|
|
899
|
+
return request.idea.market.symbol ?? request.idea.market.universe?.[0] ?? "UNKNOWN";
|
|
900
|
+
}
|
|
901
|
+
function actionForPosition(target) {
|
|
902
|
+
if (target > 0) return "long";
|
|
903
|
+
if (target < 0) return "short";
|
|
904
|
+
return "flat";
|
|
905
|
+
}
|
|
906
|
+
function simulate(params) {
|
|
907
|
+
const prices = closePrices(params.bars);
|
|
908
|
+
const initialEquity = params.request.assumptions.initialEquityUsd ?? 1e4;
|
|
909
|
+
const symbol = symbolForRequest(params.request);
|
|
910
|
+
let equity = initialEquity;
|
|
911
|
+
let peak = initialEquity;
|
|
912
|
+
let maxDrawdown = 0;
|
|
913
|
+
let previousPosition = 0;
|
|
914
|
+
let turnover = 0;
|
|
915
|
+
let trades = 0;
|
|
916
|
+
const decisions = [];
|
|
917
|
+
for (let index = 0; index < params.bars.length; index += 1) {
|
|
918
|
+
const targetPosition = params.positions[index] ?? 0;
|
|
919
|
+
const deltaPosition = Math.abs(targetPosition - previousPosition);
|
|
920
|
+
if (deltaPosition > 0) {
|
|
921
|
+
turnover += deltaPosition;
|
|
922
|
+
trades += 1;
|
|
923
|
+
equity -= equity * deltaPosition * (params.costBps / 1e4);
|
|
924
|
+
}
|
|
925
|
+
if (index > 0) {
|
|
926
|
+
const priceReturn = prices[index - 1] === 0 ? 0 : prices[index] / prices[index - 1] - 1;
|
|
927
|
+
equity *= 1 + previousPosition * priceReturn;
|
|
928
|
+
}
|
|
929
|
+
peak = Math.max(peak, equity);
|
|
930
|
+
maxDrawdown = Math.max(maxDrawdown, peak === 0 ? 0 : (peak - equity) / peak);
|
|
931
|
+
decisions.push({
|
|
932
|
+
time: params.bars[index].time,
|
|
933
|
+
symbol,
|
|
934
|
+
action: actionForPosition(targetPosition),
|
|
935
|
+
targetPosition,
|
|
936
|
+
reason: targetPosition === previousPosition ? "rule maintained target position" : "rule changed target position",
|
|
937
|
+
price: params.bars[index].close
|
|
938
|
+
});
|
|
939
|
+
previousPosition = targetPosition;
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
decisions: compactDecisionChanges(decisions),
|
|
943
|
+
metrics: {
|
|
944
|
+
endingEquityUsd: equity,
|
|
945
|
+
maxDrawdownPct: maxDrawdown * 100,
|
|
946
|
+
netReturnPct: (equity / initialEquity - 1) * 100,
|
|
947
|
+
turnover,
|
|
948
|
+
trades
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function runQuantIdeaTest(params) {
|
|
953
|
+
const request = quantTestRequestV1Schema.parse(params.request);
|
|
954
|
+
const bars = normalizeQuantBars(params.bars);
|
|
955
|
+
const plan = buildQuantTestPlan(request.idea);
|
|
956
|
+
const warnings = [...plan.warnings, ...quantDataWarnings({ bars, request })];
|
|
957
|
+
const supported = plan.supportedTestKinds.includes("idea_rule_simulation");
|
|
958
|
+
if (!supported) {
|
|
959
|
+
return finalizeQuantTesterReport({
|
|
960
|
+
ok: false,
|
|
961
|
+
testRunKind: "idea_rule_simulation",
|
|
962
|
+
supported: false,
|
|
963
|
+
unsupportedReason: `Family ${request.idea.family} does not support deterministic idea simulation in V1.`,
|
|
964
|
+
summary: "Idea simulation is not supported for this family.",
|
|
965
|
+
dataLineage: buildQuantDataLineage({ request, warnings }),
|
|
966
|
+
warnings
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
const evaluated = evaluateQuantRule({ bars, idea: request.idea });
|
|
970
|
+
warnings.push(...evaluated.warnings);
|
|
971
|
+
const simulation = simulate({
|
|
972
|
+
bars,
|
|
973
|
+
costBps: quantCostBps(request),
|
|
974
|
+
positions: evaluated.positions,
|
|
975
|
+
request
|
|
976
|
+
});
|
|
977
|
+
const decisionArtifact = validateDecisionArtifact({
|
|
978
|
+
version: "1",
|
|
979
|
+
family: request.idea.family,
|
|
980
|
+
symbol: symbolForRequest(request),
|
|
981
|
+
resolution: request.window.resolution,
|
|
982
|
+
decisions: simulation.decisions,
|
|
983
|
+
warnings
|
|
984
|
+
});
|
|
985
|
+
return finalizeQuantTesterReport({
|
|
986
|
+
ok: true,
|
|
987
|
+
testRunKind: "idea_rule_simulation",
|
|
988
|
+
supported: true,
|
|
989
|
+
summary: `Idea simulation completed: net return ${simulation.metrics.netReturnPct.toFixed(2)}%, trades ${simulation.metrics.trades}.`,
|
|
990
|
+
dataLineage: buildQuantDataLineage({ request, warnings }),
|
|
991
|
+
ideaSimulation: simulation.metrics,
|
|
992
|
+
decisionArtifact,
|
|
993
|
+
warnings
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/quant/tester/run-signal-study.ts
|
|
998
|
+
function horizonBars(request) {
|
|
999
|
+
const value = request.idea.rule?.params.horizonBars;
|
|
1000
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 1;
|
|
1001
|
+
}
|
|
1002
|
+
function runSignalStudy(params) {
|
|
1003
|
+
const request = quantTestRequestV1Schema.parse(params.request);
|
|
1004
|
+
const bars = normalizeQuantBars(params.bars);
|
|
1005
|
+
const plan = buildQuantTestPlan(request.idea);
|
|
1006
|
+
const warnings = [...plan.warnings, ...quantDataWarnings({ bars, request })];
|
|
1007
|
+
const supported = plan.supportedTestKinds.includes("signal_study");
|
|
1008
|
+
if (!supported) {
|
|
1009
|
+
return finalizeQuantTesterReport({
|
|
1010
|
+
ok: false,
|
|
1011
|
+
testRunKind: "signal_study",
|
|
1012
|
+
supported: false,
|
|
1013
|
+
unsupportedReason: `Family ${request.idea.family} does not support signal studies in V1.`,
|
|
1014
|
+
summary: "Signal study is not supported for this family.",
|
|
1015
|
+
dataLineage: buildQuantDataLineage({ request, warnings }),
|
|
1016
|
+
warnings
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
const evaluated = evaluateQuantRule({ bars, idea: request.idea });
|
|
1020
|
+
warnings.push(...evaluated.warnings);
|
|
1021
|
+
const prices = closePrices(bars);
|
|
1022
|
+
const horizon = horizonBars(request);
|
|
1023
|
+
const study = studyForwardReturns({
|
|
1024
|
+
condition: evaluated.condition,
|
|
1025
|
+
horizonBars: horizon,
|
|
1026
|
+
prices
|
|
1027
|
+
});
|
|
1028
|
+
const forward = forwardReturns(prices, horizon);
|
|
1029
|
+
const excursions = summarizeExcursions({
|
|
1030
|
+
bars,
|
|
1031
|
+
condition: evaluated.condition,
|
|
1032
|
+
horizonBars: horizon
|
|
1033
|
+
});
|
|
1034
|
+
const ic = informationCoefficient({
|
|
1035
|
+
forwardReturns: forward,
|
|
1036
|
+
signal: evaluated.signal
|
|
1037
|
+
});
|
|
1038
|
+
const summary = signalStudySummary({
|
|
1039
|
+
conditionedCount: study.conditioned.count,
|
|
1040
|
+
conditionedMean: study.conditioned.mean,
|
|
1041
|
+
unconditionalMean: study.unconditional.mean
|
|
1042
|
+
});
|
|
1043
|
+
return finalizeQuantTesterReport({
|
|
1044
|
+
ok: true,
|
|
1045
|
+
testRunKind: "signal_study",
|
|
1046
|
+
supported: true,
|
|
1047
|
+
summary,
|
|
1048
|
+
dataLineage: buildQuantDataLineage({ request, warnings }),
|
|
1049
|
+
signalStudy: {
|
|
1050
|
+
forwardReturns: study,
|
|
1051
|
+
informationCoefficient: ic,
|
|
1052
|
+
excursions,
|
|
1053
|
+
signalEventCount: evaluated.condition.filter(Boolean).length
|
|
1054
|
+
},
|
|
1055
|
+
warnings
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/quant/timeframes.ts
|
|
1060
|
+
var QUANT_RESOLUTION_SECONDS = {
|
|
1061
|
+
"1": 60,
|
|
1062
|
+
"5": 300,
|
|
1063
|
+
"15": 900,
|
|
1064
|
+
"30": 1800,
|
|
1065
|
+
"60": 3600,
|
|
1066
|
+
"240": 14400,
|
|
1067
|
+
"1D": 86400,
|
|
1068
|
+
"1W": 604800
|
|
1069
|
+
};
|
|
1070
|
+
function quantResolutionToSeconds(resolution) {
|
|
1071
|
+
return QUANT_RESOLUTION_SECONDS[resolution];
|
|
1072
|
+
}
|
|
1073
|
+
function parseQuantTimeToSeconds(value) {
|
|
1074
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1075
|
+
return Math.max(0, Math.trunc(value));
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1078
|
+
const trimmed = value.trim();
|
|
1079
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(trimmed)) {
|
|
1080
|
+
return Math.max(0, Math.trunc(Number.parseFloat(trimmed)));
|
|
1081
|
+
}
|
|
1082
|
+
const parsed = new Date(trimmed);
|
|
1083
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1084
|
+
return Math.max(0, Math.trunc(parsed.getTime() / 1e3));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
function assertQuantWindow(params) {
|
|
1090
|
+
const startSeconds = parseQuantTimeToSeconds(params.timeframeStart);
|
|
1091
|
+
const endSeconds = parseQuantTimeToSeconds(params.timeframeEnd);
|
|
1092
|
+
if (startSeconds == null || endSeconds == null) {
|
|
1093
|
+
throw new Error("Quant test window must use parseable start and end times");
|
|
1094
|
+
}
|
|
1095
|
+
if (endSeconds <= startSeconds) {
|
|
1096
|
+
throw new Error("Quant test window end must be after start");
|
|
1097
|
+
}
|
|
1098
|
+
return { endSeconds, startSeconds };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export { DONCHIAN_BREAKOUT_RULE_KIND, FUNDING_CARRY_RULE_KIND, MACD_CROSSOVER_RULE_KIND, MA_CROSS_RULE_KIND, MOMENTUM_RULE_KIND, QUANT_FAMILY_CAPABILITIES, QUANT_RESOLUTION_SECONDS, RSI_MEAN_REVERSION_RULE_KIND, ZSCORE_MEAN_REVERSION_RULE_KIND, assertQuantWindow, atr, beta, bollinger, buildEventWindows, buildQuantDataLineage, buildQuantTestPlan, closePrices, compactDecisionChanges, correlation, cumulativeFunding, donchian, ema, evaluateQuantRule, finalizeQuantTesterReport, forwardReturns, fundingRates, hitRate, informationCoefficient, logReturns, macd, momentum, normalizeQuantBars, parseQuantTimeToSeconds, quantBarSchema, quantCostBps, quantDataWarnings, quantDecisionActionSchema, quantDecisionArtifactV1Schema, quantDecisionSchema, quantFeatureSpecSchema, quantIdeaSpecV1Schema, quantResolutionSchema, quantResolutionToSeconds, quantRuleSpecSchema, quantStrategyFamilySchema, quantTestKindSchema, quantTestRequestV1Schema, quantTesterReportV1Schema, relativeStrength, relativeVolume, resolveQuantFamilyCapability, rollingCorrelation, rollingVolatility, rollingZScore, rsi, runQuantIdeaTest, runSignalStudy, signalStudySummary, simpleReturns, sliceBarsToWindow, sma, studyForwardReturns, summarizeExcursions, summarizeNumbers, trueRanges, typicalPrices, validateDecisionArtifact, volumes };
|
|
1102
|
+
//# sourceMappingURL=index.js.map
|
|
1103
|
+
//# sourceMappingURL=index.js.map
|