@wicklib/pro 0.0.0-stage → 0.11.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/LICENSE +93 -0
- package/README.md +51 -2
- package/dist/index.cjs +1020 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +593 -0
- package/dist/index.d.ts +593 -0
- package/dist/index.js +974 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AlertEngine: () => AlertEngine,
|
|
24
|
+
DepthAggregator: () => DepthAggregator,
|
|
25
|
+
FootprintAggregator: () => FootprintAggregator,
|
|
26
|
+
LicenseError: () => LicenseError,
|
|
27
|
+
PRO_CORE_FEATURES: () => PRO_CORE_FEATURES,
|
|
28
|
+
PRO_EXTENSIONS: () => PRO_EXTENSIONS,
|
|
29
|
+
PRO_FEATURES: () => PRO_FEATURES,
|
|
30
|
+
PRO_VERSION: () => PRO_VERSION,
|
|
31
|
+
addVolumeProfileOverlay: () => addVolumeProfileOverlay,
|
|
32
|
+
assertProFeature: () => assertProFeature,
|
|
33
|
+
computeDepth: () => computeDepth,
|
|
34
|
+
computeFootprint: () => computeFootprint,
|
|
35
|
+
computeVolumeProfile: () => computeVolumeProfile,
|
|
36
|
+
createLicenseContext: () => createLicenseContext,
|
|
37
|
+
detectAbsorption: () => detectAbsorption,
|
|
38
|
+
evaluateAlerts: () => evaluateAlerts,
|
|
39
|
+
getProExtensions: () => getProExtensions,
|
|
40
|
+
hasProFeature: () => hasProFeature,
|
|
41
|
+
runBacktest: () => runBacktest,
|
|
42
|
+
runScriptBacktest: () => runScriptBacktest
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// package.json
|
|
47
|
+
var package_default = {
|
|
48
|
+
name: "@wicklib/pro",
|
|
49
|
+
version: "0.11.0",
|
|
50
|
+
description: "Wicklib commercial add-on",
|
|
51
|
+
license: "SEE LICENSE IN LICENSE",
|
|
52
|
+
type: "module",
|
|
53
|
+
sideEffects: false,
|
|
54
|
+
files: [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md"
|
|
57
|
+
],
|
|
58
|
+
main: "./dist/index.cjs",
|
|
59
|
+
module: "./dist/index.js",
|
|
60
|
+
types: "./dist/index.d.ts",
|
|
61
|
+
exports: {
|
|
62
|
+
".": {
|
|
63
|
+
import: {
|
|
64
|
+
types: "./dist/index.d.ts",
|
|
65
|
+
default: "./dist/index.js"
|
|
66
|
+
},
|
|
67
|
+
require: {
|
|
68
|
+
types: "./dist/index.d.cts",
|
|
69
|
+
default: "./dist/index.cjs"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"./package.json": "./package.json"
|
|
73
|
+
},
|
|
74
|
+
publishConfig: {
|
|
75
|
+
access: "public"
|
|
76
|
+
},
|
|
77
|
+
engines: {
|
|
78
|
+
node: ">=24.0.0"
|
|
79
|
+
},
|
|
80
|
+
repository: {
|
|
81
|
+
type: "git",
|
|
82
|
+
url: "git+https://github.com/wicklib/wicklib-pro.git",
|
|
83
|
+
directory: "packages/pro"
|
|
84
|
+
},
|
|
85
|
+
peerDependencies: {
|
|
86
|
+
"@wicklib/core": "^0.11.0"
|
|
87
|
+
},
|
|
88
|
+
scripts: {
|
|
89
|
+
build: "tsup",
|
|
90
|
+
typecheck: "tsc --noEmit",
|
|
91
|
+
test: "vitest run"
|
|
92
|
+
},
|
|
93
|
+
devDependencies: {
|
|
94
|
+
tsup: "^9.0.0",
|
|
95
|
+
typescript: "^5.9.3",
|
|
96
|
+
vitest: "^5.0.5"
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/version.ts
|
|
101
|
+
var PRO_VERSION = package_default.version;
|
|
102
|
+
|
|
103
|
+
// src/script/backtest.ts
|
|
104
|
+
var import_core2 = require("@wicklib/core");
|
|
105
|
+
|
|
106
|
+
// src/strategy/backtest.ts
|
|
107
|
+
var import_core = require("@wicklib/core");
|
|
108
|
+
var DEFAULT_OPTIONS = {
|
|
109
|
+
feeRate: 0,
|
|
110
|
+
slippage: 0
|
|
111
|
+
};
|
|
112
|
+
function runBacktest(context, bars, strategy, options) {
|
|
113
|
+
(0, import_core.assertFeature)(context, "strategy");
|
|
114
|
+
const initialCapital = options.initialCapital;
|
|
115
|
+
const feeRate = options.feeRate ?? DEFAULT_OPTIONS.feeRate;
|
|
116
|
+
const slippage = options.slippage ?? DEFAULT_OPTIONS.slippage;
|
|
117
|
+
if (!Number.isFinite(initialCapital) || initialCapital <= 0) {
|
|
118
|
+
throw new import_core.WickError("INVALID_ARGUMENT", "Backtest initialCapital must be positive");
|
|
119
|
+
}
|
|
120
|
+
if (!Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1) {
|
|
121
|
+
throw new import_core.WickError("INVALID_ARGUMENT", "Backtest feeRate must be in [0, 1)");
|
|
122
|
+
}
|
|
123
|
+
if (!Number.isFinite(slippage) || slippage < 0 || slippage >= 1) {
|
|
124
|
+
throw new import_core.WickError("INVALID_ARGUMENT", "Backtest slippage must be in [0, 1)");
|
|
125
|
+
}
|
|
126
|
+
if (typeof strategy !== "function") {
|
|
127
|
+
throw new import_core.WickError("INVALID_ARGUMENT", "Backtest strategy must be a function");
|
|
128
|
+
}
|
|
129
|
+
let cash = initialCapital;
|
|
130
|
+
let units = 0;
|
|
131
|
+
let position = 0;
|
|
132
|
+
let pending = null;
|
|
133
|
+
let previousTime = Number.NEGATIVE_INFINITY;
|
|
134
|
+
const fills = [];
|
|
135
|
+
const equity = [];
|
|
136
|
+
let peak = initialCapital;
|
|
137
|
+
let maxDrawdown = 0;
|
|
138
|
+
for (let index = 0; index < bars.length; index += 1) {
|
|
139
|
+
const bar = bars[index];
|
|
140
|
+
if (!Number.isFinite(bar.time) || bar.time <= previousTime || !Number.isFinite(bar.open) || !Number.isFinite(bar.close) || !Number.isFinite(bar.high) || !Number.isFinite(bar.low)) {
|
|
141
|
+
throw new import_core.WickError("INVALID_DATA", "Backtest bars require ascending finite OHLC values");
|
|
142
|
+
}
|
|
143
|
+
previousTime = bar.time;
|
|
144
|
+
if (pending !== null && pending !== position) {
|
|
145
|
+
const side = pending > position ? "buy" : "sell";
|
|
146
|
+
const price = side === "buy" ? bar.open * (1 + slippage) : bar.open * (1 - slippage);
|
|
147
|
+
const equityAtFill = cash + units * price;
|
|
148
|
+
const desiredUnits = pending * equityAtFill / price;
|
|
149
|
+
const quantity = desiredUnits - units;
|
|
150
|
+
const fee = Math.abs(quantity * price) * feeRate;
|
|
151
|
+
cash -= quantity * price + fee;
|
|
152
|
+
units = desiredUnits;
|
|
153
|
+
position = pending;
|
|
154
|
+
if (quantity !== 0) {
|
|
155
|
+
fills.push({ time: bar.time, side, price, quantity, fee });
|
|
156
|
+
}
|
|
157
|
+
pending = null;
|
|
158
|
+
}
|
|
159
|
+
const markedEquity = cash + units * bar.close;
|
|
160
|
+
peak = Math.max(peak, markedEquity);
|
|
161
|
+
if (peak > 0) {
|
|
162
|
+
maxDrawdown = Math.max(maxDrawdown, (peak - markedEquity) / peak);
|
|
163
|
+
}
|
|
164
|
+
equity.push({
|
|
165
|
+
time: bar.time,
|
|
166
|
+
cash,
|
|
167
|
+
units,
|
|
168
|
+
position,
|
|
169
|
+
price: bar.close,
|
|
170
|
+
equity: markedEquity
|
|
171
|
+
});
|
|
172
|
+
const signal = strategy({
|
|
173
|
+
bar,
|
|
174
|
+
index,
|
|
175
|
+
position,
|
|
176
|
+
cash,
|
|
177
|
+
units,
|
|
178
|
+
equity: markedEquity
|
|
179
|
+
});
|
|
180
|
+
if (signal !== void 0 && signal !== null) {
|
|
181
|
+
if (signal !== -1 && signal !== 0 && signal !== 1) {
|
|
182
|
+
throw new import_core.WickError("INVALID_DATA", "Strategy target must be -1, 0, or 1");
|
|
183
|
+
}
|
|
184
|
+
pending = signal;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const finalEquity = equity.at(-1)?.equity ?? initialCapital;
|
|
188
|
+
return {
|
|
189
|
+
initialCapital,
|
|
190
|
+
finalEquity,
|
|
191
|
+
returnPercent: (finalEquity - initialCapital) / initialCapital * 100,
|
|
192
|
+
maxDrawdown,
|
|
193
|
+
fills,
|
|
194
|
+
equity
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/script/backtest.ts
|
|
199
|
+
async function runScriptBacktest(context, script, bars, options) {
|
|
200
|
+
(0, import_core2.assertFeature)(context, "strategy");
|
|
201
|
+
if (!script || typeof script.replay !== "function") {
|
|
202
|
+
throw new import_core2.WickError("INVALID_ARGUMENT", "Script adapter requires a replay function");
|
|
203
|
+
}
|
|
204
|
+
if (typeof options.signalPlot !== "string" || options.signalPlot.trim() === "") {
|
|
205
|
+
throw new import_core2.WickError("INVALID_ARGUMENT", "Script signalPlot must be a non-empty string");
|
|
206
|
+
}
|
|
207
|
+
const result = await script.replay(bars);
|
|
208
|
+
const values = result?.plots?.[options.signalPlot];
|
|
209
|
+
if (!Array.isArray(values)) {
|
|
210
|
+
throw new import_core2.WickError("INVALID_DATA", `Script plot not found: ${options.signalPlot}`);
|
|
211
|
+
}
|
|
212
|
+
if (values.length !== bars.length) {
|
|
213
|
+
throw new import_core2.WickError("INVALID_DATA", "Script signal plot length must match the bar count");
|
|
214
|
+
}
|
|
215
|
+
if (result.alerts.length > 0 && !result.alertEvents) {
|
|
216
|
+
throw new import_core2.WickError("INVALID_DATA", "Script alerts are missing their bar events");
|
|
217
|
+
}
|
|
218
|
+
const alerts = (result.alertEvents ?? []).map((event) => validateAlert(event));
|
|
219
|
+
const backtest = runBacktest(
|
|
220
|
+
context,
|
|
221
|
+
bars,
|
|
222
|
+
({ index }) => {
|
|
223
|
+
const value = values[index];
|
|
224
|
+
if (value === null || value === void 0) {
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
if (value !== -1 && value !== 0 && value !== 1) {
|
|
228
|
+
throw new import_core2.WickError("INVALID_DATA", "Script signal must be -1, 0, 1, or null");
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
},
|
|
232
|
+
options
|
|
233
|
+
);
|
|
234
|
+
return { ...backtest, alerts };
|
|
235
|
+
}
|
|
236
|
+
function validateAlert(event) {
|
|
237
|
+
if (!event || !Number.isFinite(event.time) || !Number.isInteger(event.barIndex) || event.barIndex < 0 || typeof event.message !== "string") {
|
|
238
|
+
throw new import_core2.WickError("INVALID_DATA", "Invalid script alert event");
|
|
239
|
+
}
|
|
240
|
+
return { time: event.time, barIndex: event.barIndex, message: event.message };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/alerts/engine.ts
|
|
244
|
+
var import_core3 = require("@wicklib/core");
|
|
245
|
+
var SOURCES = /* @__PURE__ */ new Set(["close", "high", "low"]);
|
|
246
|
+
var AlertEngine = class {
|
|
247
|
+
rules;
|
|
248
|
+
previous = /* @__PURE__ */ new Map();
|
|
249
|
+
lastTime = Number.NEGATIVE_INFINITY;
|
|
250
|
+
constructor(context, rules) {
|
|
251
|
+
(0, import_core3.assertFeature)(context, "alerts");
|
|
252
|
+
const ids = /* @__PURE__ */ new Set();
|
|
253
|
+
for (const rule of rules) {
|
|
254
|
+
if (!rule || typeof rule.id !== "string" || rule.id.trim() === "" || !Number.isFinite(rule.price) || rule.direction !== "above" && rule.direction !== "below" && rule.direction !== "cross" || rule.source !== void 0 && !SOURCES.has(rule.source)) {
|
|
255
|
+
throw new import_core3.WickError("INVALID_ARGUMENT", "Invalid price alert rule");
|
|
256
|
+
}
|
|
257
|
+
if (ids.has(rule.id)) {
|
|
258
|
+
throw new import_core3.WickError("INVALID_ARGUMENT", `Duplicate alert rule id: ${rule.id}`);
|
|
259
|
+
}
|
|
260
|
+
ids.add(rule.id);
|
|
261
|
+
}
|
|
262
|
+
this.rules = rules.map((rule) => ({ ...rule }));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Consumes one closed bar and returns the alerts triggered by that bar.
|
|
266
|
+
*/
|
|
267
|
+
update(bar) {
|
|
268
|
+
if (!Number.isFinite(bar.time)) {
|
|
269
|
+
throw new import_core3.WickError("INVALID_DATA", "Alert bar requires a finite time");
|
|
270
|
+
}
|
|
271
|
+
if (bar.time <= this.lastTime) {
|
|
272
|
+
throw new import_core3.WickError("INVALID_DATA", "Alert bars must have increasing times");
|
|
273
|
+
}
|
|
274
|
+
this.lastTime = bar.time;
|
|
275
|
+
const triggers = [];
|
|
276
|
+
for (const rule of this.rules) {
|
|
277
|
+
const source = rule.source ?? "close";
|
|
278
|
+
const value = source === "close" ? bar.close : source === "high" ? bar.high : bar.low;
|
|
279
|
+
const previous = this.previous.get(rule.id);
|
|
280
|
+
if (previous !== void 0) {
|
|
281
|
+
if (rule.direction === "above" && previous <= rule.price && value > rule.price) {
|
|
282
|
+
triggers.push(trigger(rule, bar, value, "above"));
|
|
283
|
+
} else if (rule.direction === "below" && previous >= rule.price && value < rule.price) {
|
|
284
|
+
triggers.push(trigger(rule, bar, value, "below"));
|
|
285
|
+
} else if (rule.direction === "cross") {
|
|
286
|
+
if (previous <= rule.price && value > rule.price) {
|
|
287
|
+
triggers.push(trigger(rule, bar, value, "above"));
|
|
288
|
+
} else if (previous >= rule.price && value < rule.price) {
|
|
289
|
+
triggers.push(trigger(rule, bar, value, "below"));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
this.previous.set(rule.id, value);
|
|
294
|
+
}
|
|
295
|
+
return triggers;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Clears previous values so the next bar initializes state again.
|
|
299
|
+
*/
|
|
300
|
+
reset() {
|
|
301
|
+
this.previous.clear();
|
|
302
|
+
this.lastTime = Number.NEGATIVE_INFINITY;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function evaluateAlerts(context, rules, bars) {
|
|
306
|
+
const engine = new AlertEngine(context, rules);
|
|
307
|
+
const triggers = [];
|
|
308
|
+
for (const bar of bars) {
|
|
309
|
+
triggers.push(...engine.update(bar));
|
|
310
|
+
}
|
|
311
|
+
return triggers;
|
|
312
|
+
}
|
|
313
|
+
function trigger(rule, bar, value, direction) {
|
|
314
|
+
return {
|
|
315
|
+
ruleId: rule.id,
|
|
316
|
+
time: bar.time,
|
|
317
|
+
direction,
|
|
318
|
+
price: rule.price,
|
|
319
|
+
value,
|
|
320
|
+
bar
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/analytics/absorption.ts
|
|
325
|
+
var import_core6 = require("@wicklib/core");
|
|
326
|
+
|
|
327
|
+
// src/analytics/depth.ts
|
|
328
|
+
var import_core4 = require("@wicklib/core");
|
|
329
|
+
var DEFAULT_IMBALANCE_RATIO = 2;
|
|
330
|
+
var DepthAggregator = class {
|
|
331
|
+
options;
|
|
332
|
+
bars = /* @__PURE__ */ new Map();
|
|
333
|
+
lastTime = Number.NEGATIVE_INFINITY;
|
|
334
|
+
constructor(context, options) {
|
|
335
|
+
(0, import_core4.assertFeature)(context, "orderflow");
|
|
336
|
+
if (options.priceStep === void 0 || !Number.isFinite(options.priceStep) || options.priceStep <= 0) {
|
|
337
|
+
throw new import_core4.WickError("INVALID_ARGUMENT", "Depth priceStep must be a positive number");
|
|
338
|
+
}
|
|
339
|
+
const ratio = options.imbalanceRatio ?? DEFAULT_IMBALANCE_RATIO;
|
|
340
|
+
if (!Number.isFinite(ratio) || ratio < 1) {
|
|
341
|
+
throw new import_core4.WickError("INVALID_ARGUMENT", "Depth imbalanceRatio must be at least 1");
|
|
342
|
+
}
|
|
343
|
+
this.options = { priceStep: options.priceStep, imbalanceRatio: ratio };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Adds or replaces one complete snapshot and returns its aggregated bar.
|
|
347
|
+
*/
|
|
348
|
+
push(snapshot) {
|
|
349
|
+
if (!Number.isFinite(snapshot.time) || !Array.isArray(snapshot.bids) || !Array.isArray(snapshot.asks)) {
|
|
350
|
+
throw new import_core4.WickError("INVALID_DATA", "Depth snapshots require finite time, bids, and asks");
|
|
351
|
+
}
|
|
352
|
+
if (snapshot.time < this.lastTime) {
|
|
353
|
+
throw new import_core4.WickError("INVALID_DATA", "Depth snapshots must be in non-decreasing time order");
|
|
354
|
+
}
|
|
355
|
+
this.lastTime = snapshot.time;
|
|
356
|
+
const levels = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const [side, entries] of [
|
|
358
|
+
["bid", snapshot.bids],
|
|
359
|
+
["ask", snapshot.asks]
|
|
360
|
+
]) {
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
if (!Number.isFinite(entry.price) || !Number.isFinite(entry.size) || entry.size < 0) {
|
|
363
|
+
throw new import_core4.WickError(
|
|
364
|
+
"INVALID_DATA",
|
|
365
|
+
"Depth levels require finite price and non-negative size"
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const price = Math.floor(entry.price / this.options.priceStep) * this.options.priceStep;
|
|
369
|
+
let level = levels.get(price);
|
|
370
|
+
if (!level) {
|
|
371
|
+
level = { bid: 0, ask: 0 };
|
|
372
|
+
levels.set(price, level);
|
|
373
|
+
}
|
|
374
|
+
if (side === "bid") level.bid += entry.size;
|
|
375
|
+
else level.ask += entry.size;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
let bidSize = 0;
|
|
379
|
+
let askSize = 0;
|
|
380
|
+
const sortedLevels = [...levels.entries()].sort(([first], [second]) => first - second).map(([priceLow, level]) => {
|
|
381
|
+
bidSize += level.bid;
|
|
382
|
+
askSize += level.ask;
|
|
383
|
+
return {
|
|
384
|
+
priceLow,
|
|
385
|
+
priceHigh: priceLow + this.options.priceStep,
|
|
386
|
+
bidSize: level.bid,
|
|
387
|
+
askSize: level.ask,
|
|
388
|
+
totalSize: level.bid + level.ask,
|
|
389
|
+
delta: level.bid - level.ask,
|
|
390
|
+
imbalance: imbalanceOf(level.bid, level.ask, this.options.imbalanceRatio)
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
const bar = {
|
|
394
|
+
time: snapshot.time,
|
|
395
|
+
priceLow: sortedLevels[0]?.priceLow ?? 0,
|
|
396
|
+
priceHigh: sortedLevels.at(-1)?.priceHigh ?? 0,
|
|
397
|
+
bidSize,
|
|
398
|
+
askSize,
|
|
399
|
+
totalSize: bidSize + askSize,
|
|
400
|
+
delta: bidSize - askSize,
|
|
401
|
+
imbalance: imbalanceOf(bidSize, askSize, this.options.imbalanceRatio),
|
|
402
|
+
levels: sortedLevels
|
|
403
|
+
};
|
|
404
|
+
this.bars.set(snapshot.time, bar);
|
|
405
|
+
return bar;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Returns the latest snapshot at or before a time.
|
|
409
|
+
*/
|
|
410
|
+
latest(time = Number.POSITIVE_INFINITY) {
|
|
411
|
+
const eligible = [...this.bars.values()].filter((bar) => bar.time <= time).sort((first, second) => first.time - second.time);
|
|
412
|
+
return eligible.at(-1) ?? null;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Returns all snapshots in ascending time order.
|
|
416
|
+
*/
|
|
417
|
+
getBars() {
|
|
418
|
+
return [...this.bars.values()].sort((first, second) => first.time - second.time);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Clears all snapshots and the streaming order guard.
|
|
422
|
+
*/
|
|
423
|
+
reset() {
|
|
424
|
+
this.bars.clear();
|
|
425
|
+
this.lastTime = Number.NEGATIVE_INFINITY;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
function computeDepth(context, snapshots, options) {
|
|
429
|
+
const aggregator = new DepthAggregator(context, options);
|
|
430
|
+
for (const snapshot of snapshots) {
|
|
431
|
+
aggregator.push(snapshot);
|
|
432
|
+
}
|
|
433
|
+
return aggregator.getBars();
|
|
434
|
+
}
|
|
435
|
+
function imbalanceOf(bid, ask, ratio) {
|
|
436
|
+
if (bid === 0 && ask === 0) return null;
|
|
437
|
+
if (ask === 0) return "bid";
|
|
438
|
+
if (bid === 0) return "ask";
|
|
439
|
+
if (bid >= ask * ratio) return "bid";
|
|
440
|
+
if (ask >= bid * ratio) return "ask";
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/analytics/orderflow.ts
|
|
445
|
+
var import_core5 = require("@wicklib/core");
|
|
446
|
+
var DEFAULT_BUCKET_MS = 6e4;
|
|
447
|
+
var DEFAULT_IMBALANCE_RATIO2 = 3;
|
|
448
|
+
var FootprintAggregator = class {
|
|
449
|
+
options;
|
|
450
|
+
levels = /* @__PURE__ */ new Map();
|
|
451
|
+
lastTradeTime = Number.NEGATIVE_INFINITY;
|
|
452
|
+
constructor(context, options) {
|
|
453
|
+
(0, import_core5.assertFeature)(context, "orderflow");
|
|
454
|
+
this.options = resolveOptions(options);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Adds one trade and returns the updated bar for its time bucket.
|
|
458
|
+
*/
|
|
459
|
+
push(trade) {
|
|
460
|
+
this.validateTrade(trade);
|
|
461
|
+
if (trade.time < this.lastTradeTime) {
|
|
462
|
+
throw new import_core5.WickError("INVALID_DATA", "Streaming trades must be in non-decreasing time order");
|
|
463
|
+
}
|
|
464
|
+
this.lastTradeTime = trade.time;
|
|
465
|
+
const time = Math.floor(trade.time / this.options.bucketMs) * this.options.bucketMs;
|
|
466
|
+
const price = Math.floor(trade.price / this.options.priceStep) * this.options.priceStep;
|
|
467
|
+
let levels = this.levels.get(time);
|
|
468
|
+
if (!levels) {
|
|
469
|
+
levels = /* @__PURE__ */ new Map();
|
|
470
|
+
this.levels.set(time, levels);
|
|
471
|
+
}
|
|
472
|
+
let level = levels.get(price);
|
|
473
|
+
if (!level) {
|
|
474
|
+
level = { buy: 0, sell: 0 };
|
|
475
|
+
levels.set(price, level);
|
|
476
|
+
}
|
|
477
|
+
if (trade.side === "buy") level.buy += trade.size;
|
|
478
|
+
else level.sell += trade.size;
|
|
479
|
+
return this.buildBar(time);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Adds multiple trades in input order.
|
|
483
|
+
*/
|
|
484
|
+
pushMany(trades) {
|
|
485
|
+
for (const trade of trades) {
|
|
486
|
+
this.push(trade);
|
|
487
|
+
}
|
|
488
|
+
return this.getBars();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Returns a bar by bucket start time, or any time inside that bucket.
|
|
492
|
+
*/
|
|
493
|
+
getBar(time) {
|
|
494
|
+
const bucket = Math.floor(time / this.options.bucketMs) * this.options.bucketMs;
|
|
495
|
+
return this.buildBar(bucket);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Returns all completed/current buckets in ascending time order.
|
|
499
|
+
*/
|
|
500
|
+
getBars() {
|
|
501
|
+
return [...this.levels.keys()].sort((first, second) => first - second).map((time) => this.buildBar(time)).filter((bar) => bar !== null);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Clears all trades and resets the streaming order guard.
|
|
505
|
+
*/
|
|
506
|
+
reset() {
|
|
507
|
+
this.levels.clear();
|
|
508
|
+
this.lastTradeTime = Number.NEGATIVE_INFINITY;
|
|
509
|
+
}
|
|
510
|
+
buildBar(time) {
|
|
511
|
+
const levels = this.levels.get(time);
|
|
512
|
+
if (!levels) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
let buyVolume = 0;
|
|
516
|
+
let sellVolume = 0;
|
|
517
|
+
const sortedLevels = [...levels.entries()].sort(([first], [second]) => first - second).map(([priceLow, volumes]) => {
|
|
518
|
+
buyVolume += volumes.buy;
|
|
519
|
+
sellVolume += volumes.sell;
|
|
520
|
+
return {
|
|
521
|
+
priceLow,
|
|
522
|
+
priceHigh: priceLow + this.options.priceStep,
|
|
523
|
+
buyVolume: volumes.buy,
|
|
524
|
+
sellVolume: volumes.sell,
|
|
525
|
+
totalVolume: volumes.buy + volumes.sell,
|
|
526
|
+
delta: volumes.buy - volumes.sell,
|
|
527
|
+
imbalance: imbalanceOf2(volumes.buy, volumes.sell, this.options.imbalanceRatio)
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
time,
|
|
532
|
+
priceLow: sortedLevels[0]?.priceLow ?? 0,
|
|
533
|
+
priceHigh: sortedLevels.at(-1)?.priceHigh ?? 0,
|
|
534
|
+
buyVolume,
|
|
535
|
+
sellVolume,
|
|
536
|
+
totalVolume: buyVolume + sellVolume,
|
|
537
|
+
delta: buyVolume - sellVolume,
|
|
538
|
+
levels: sortedLevels
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
validateTrade(trade) {
|
|
542
|
+
if (!Number.isFinite(trade.time) || !Number.isFinite(trade.price) || !Number.isFinite(trade.size) || trade.size <= 0 || trade.price < 0 || trade.side !== "buy" && trade.side !== "sell") {
|
|
543
|
+
throw new import_core5.WickError("INVALID_DATA", "Trade ticks require finite time, price, size, and side");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
function computeFootprint(context, trades, options) {
|
|
548
|
+
const aggregator = new FootprintAggregator(context, options);
|
|
549
|
+
return aggregator.pushMany([...trades].sort((first, second) => first.time - second.time));
|
|
550
|
+
}
|
|
551
|
+
function resolveOptions(options) {
|
|
552
|
+
const bucketMs = options.bucketMs ?? DEFAULT_BUCKET_MS;
|
|
553
|
+
const priceStep = options.priceStep;
|
|
554
|
+
const imbalanceRatio = options.imbalanceRatio ?? DEFAULT_IMBALANCE_RATIO2;
|
|
555
|
+
if (!Number.isInteger(bucketMs) || bucketMs <= 0) {
|
|
556
|
+
throw new import_core5.WickError("INVALID_ARGUMENT", "Footprint bucketMs must be a positive integer");
|
|
557
|
+
}
|
|
558
|
+
if (priceStep === void 0 || !Number.isFinite(priceStep) || priceStep <= 0) {
|
|
559
|
+
throw new import_core5.WickError("INVALID_ARGUMENT", "Footprint priceStep must be a positive number");
|
|
560
|
+
}
|
|
561
|
+
if (!Number.isFinite(imbalanceRatio) || imbalanceRatio < 1) {
|
|
562
|
+
throw new import_core5.WickError("INVALID_ARGUMENT", "Footprint imbalanceRatio must be at least 1");
|
|
563
|
+
}
|
|
564
|
+
return { bucketMs, priceStep, imbalanceRatio };
|
|
565
|
+
}
|
|
566
|
+
function imbalanceOf2(buy, sell, ratio) {
|
|
567
|
+
if (buy === 0 && sell === 0) return null;
|
|
568
|
+
if (sell === 0) return "buy";
|
|
569
|
+
if (buy === 0) return "sell";
|
|
570
|
+
if (buy >= sell * ratio) return "buy";
|
|
571
|
+
if (sell >= buy * ratio) return "sell";
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/analytics/absorption.ts
|
|
576
|
+
var DEFAULT_BUCKET_MS2 = 6e4;
|
|
577
|
+
var DEFAULT_ABSORPTION_RATIO = 2;
|
|
578
|
+
function detectAbsorption(context, trades, snapshots, options) {
|
|
579
|
+
(0, import_core6.assertFeature)(context, "orderflow");
|
|
580
|
+
const bucketMs = options.bucketMs ?? DEFAULT_BUCKET_MS2;
|
|
581
|
+
const absorptionRatio = options.absorptionRatio ?? DEFAULT_ABSORPTION_RATIO;
|
|
582
|
+
const priceStep = options.priceStep;
|
|
583
|
+
if (!Number.isInteger(bucketMs) || bucketMs <= 0) {
|
|
584
|
+
throw new import_core6.WickError("INVALID_ARGUMENT", "Absorption bucketMs must be a positive integer");
|
|
585
|
+
}
|
|
586
|
+
if (!Number.isFinite(absorptionRatio) || absorptionRatio < 1) {
|
|
587
|
+
throw new import_core6.WickError("INVALID_ARGUMENT", "Absorption ratio must be at least 1");
|
|
588
|
+
}
|
|
589
|
+
if (priceStep === void 0 || !Number.isFinite(priceStep) || priceStep <= 0) {
|
|
590
|
+
throw new import_core6.WickError("INVALID_ARGUMENT", "Absorption priceStep must be a positive number");
|
|
591
|
+
}
|
|
592
|
+
const footprints = new FootprintAggregator(context, {
|
|
593
|
+
bucketMs,
|
|
594
|
+
priceStep
|
|
595
|
+
});
|
|
596
|
+
for (const trade of [...trades].sort((first, second) => first.time - second.time)) {
|
|
597
|
+
footprints.push(trade);
|
|
598
|
+
}
|
|
599
|
+
const depth = new DepthAggregator(context, { priceStep });
|
|
600
|
+
for (const snapshot of [...snapshots].sort((first, second) => first.time - second.time)) {
|
|
601
|
+
depth.push(snapshot);
|
|
602
|
+
}
|
|
603
|
+
const events = [];
|
|
604
|
+
for (const bar of footprints.getBars()) {
|
|
605
|
+
const book = depth.latest(bar.time + bucketMs - 1);
|
|
606
|
+
if (!book) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const depthByPrice = new Map(book.levels.map((level) => [level.priceLow, level]));
|
|
610
|
+
for (const level of bar.levels) {
|
|
611
|
+
const depthLevel = depthByPrice.get(level.priceLow);
|
|
612
|
+
if (!depthLevel) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const candidates = [
|
|
616
|
+
{ side: "buy", aggressive: level.buyVolume, passive: depthLevel.askSize },
|
|
617
|
+
{ side: "sell", aggressive: level.sellVolume, passive: depthLevel.bidSize }
|
|
618
|
+
];
|
|
619
|
+
for (const candidate of candidates) {
|
|
620
|
+
if (candidate.passive <= 0 || candidate.aggressive <= 0) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const ratio = candidate.aggressive / candidate.passive;
|
|
624
|
+
if (ratio >= absorptionRatio) {
|
|
625
|
+
events.push({
|
|
626
|
+
time: bar.time,
|
|
627
|
+
priceLow: level.priceLow,
|
|
628
|
+
priceHigh: level.priceHigh,
|
|
629
|
+
side: candidate.side,
|
|
630
|
+
aggressiveVolume: candidate.aggressive,
|
|
631
|
+
passiveVolume: candidate.passive,
|
|
632
|
+
ratio
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return events;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/analytics/volume-profile.ts
|
|
642
|
+
var import_core7 = require("@wicklib/core");
|
|
643
|
+
var DEFAULT_OPTIONS2 = {
|
|
644
|
+
bins: 24,
|
|
645
|
+
valueArea: 0.7,
|
|
646
|
+
distribution: "uniform"
|
|
647
|
+
};
|
|
648
|
+
function computeVolumeProfile(context, bars, options = {}) {
|
|
649
|
+
(0, import_core7.assertFeature)(context, "volume-profile");
|
|
650
|
+
const binCount = options.bins ?? DEFAULT_OPTIONS2.bins;
|
|
651
|
+
const valueAreaFraction = options.valueArea ?? DEFAULT_OPTIONS2.valueArea;
|
|
652
|
+
const distribution = options.distribution ?? DEFAULT_OPTIONS2.distribution;
|
|
653
|
+
if (!Number.isInteger(binCount) || binCount <= 0 || binCount > 2e3) {
|
|
654
|
+
throw new import_core7.WickError("INVALID_ARGUMENT", "Volume profile bins must be between 1 and 2000");
|
|
655
|
+
}
|
|
656
|
+
if (!Number.isFinite(valueAreaFraction) || valueAreaFraction <= 0 || valueAreaFraction > 1) {
|
|
657
|
+
throw new import_core7.WickError("INVALID_ARGUMENT", "Volume profile valueArea must be in (0, 1]");
|
|
658
|
+
}
|
|
659
|
+
let min;
|
|
660
|
+
let max;
|
|
661
|
+
if (options.priceRange) {
|
|
662
|
+
min = options.priceRange.min;
|
|
663
|
+
max = options.priceRange.max;
|
|
664
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
665
|
+
throw new import_core7.WickError("INVALID_ARGUMENT", "Volume profile priceRange must have max > min");
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
min = Number.POSITIVE_INFINITY;
|
|
669
|
+
max = Number.NEGATIVE_INFINITY;
|
|
670
|
+
for (const bar of bars) {
|
|
671
|
+
if (!Number.isFinite(bar.volume) || bar.volume <= 0) continue;
|
|
672
|
+
min = Math.min(min, bar.low);
|
|
673
|
+
max = Math.max(max, bar.high);
|
|
674
|
+
}
|
|
675
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
676
|
+
throw new import_core7.WickError("INVALID_DATA", "Volume profile requires bars with volume");
|
|
677
|
+
}
|
|
678
|
+
if (max <= min) {
|
|
679
|
+
max = min + 1;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const width = (max - min) / binCount;
|
|
683
|
+
const volumes = new Array(binCount).fill(0);
|
|
684
|
+
for (const bar of bars) {
|
|
685
|
+
const volume = bar.volume;
|
|
686
|
+
if (!Number.isFinite(volume) || volume === void 0 || volume <= 0) continue;
|
|
687
|
+
if (distribution === "close") {
|
|
688
|
+
const index = clampIndex(Math.floor((bar.close - min) / width), binCount);
|
|
689
|
+
volumes[index] += volume;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const low = Math.max(bar.low, min);
|
|
693
|
+
const high = Math.min(bar.high, max);
|
|
694
|
+
if (high <= low) {
|
|
695
|
+
const index = clampIndex(Math.floor((bar.close - min) / width), binCount);
|
|
696
|
+
volumes[index] += volume;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const first = clampIndex(Math.floor((low - min) / width), binCount);
|
|
700
|
+
const last = clampIndex(Math.floor((high - min) / width), binCount);
|
|
701
|
+
let assigned = 0;
|
|
702
|
+
for (let index = first; index <= last; index += 1) {
|
|
703
|
+
const binLow = min + index * width;
|
|
704
|
+
const binHigh = index === binCount - 1 ? max : binLow + width;
|
|
705
|
+
const overlap = Math.max(0, Math.min(high, binHigh) - Math.max(low, binLow));
|
|
706
|
+
const part = index === last ? volume - assigned : volume * overlap / (high - low);
|
|
707
|
+
volumes[index] += part;
|
|
708
|
+
assigned += part;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const totalVolume = volumes.reduce((sum, value) => sum + value, 0);
|
|
712
|
+
if (!(totalVolume > 0)) {
|
|
713
|
+
throw new import_core7.WickError("INVALID_DATA", "Volume profile requires bars with volume");
|
|
714
|
+
}
|
|
715
|
+
const bins = volumes.map((volume, index) => ({
|
|
716
|
+
index,
|
|
717
|
+
priceLow: min + index * width,
|
|
718
|
+
priceHigh: index === binCount - 1 ? max : min + (index + 1) * width,
|
|
719
|
+
priceMid: min + (index + 0.5) * width,
|
|
720
|
+
volume
|
|
721
|
+
}));
|
|
722
|
+
let pocIndex = 0;
|
|
723
|
+
for (let index = 1; index < bins.length; index += 1) {
|
|
724
|
+
if (bins[index].volume > bins[pocIndex].volume) {
|
|
725
|
+
pocIndex = index;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const target = totalVolume * valueAreaFraction;
|
|
729
|
+
let lowIndex = pocIndex;
|
|
730
|
+
let highIndex = pocIndex;
|
|
731
|
+
let valueVolume = bins[pocIndex].volume;
|
|
732
|
+
while (valueVolume < target && (lowIndex > 0 || highIndex < bins.length - 1)) {
|
|
733
|
+
const lower = lowIndex > 0 ? bins[lowIndex - 1].volume : -1;
|
|
734
|
+
const upper = highIndex < bins.length - 1 ? bins[highIndex + 1].volume : -1;
|
|
735
|
+
if (upper > lower) {
|
|
736
|
+
highIndex += 1;
|
|
737
|
+
valueVolume += bins[highIndex].volume;
|
|
738
|
+
} else {
|
|
739
|
+
lowIndex -= 1;
|
|
740
|
+
valueVolume += bins[lowIndex].volume;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
bins,
|
|
745
|
+
range: { min, max },
|
|
746
|
+
totalVolume,
|
|
747
|
+
poc: bins[pocIndex] ?? null,
|
|
748
|
+
valueArea: {
|
|
749
|
+
low: bins[lowIndex].priceLow,
|
|
750
|
+
high: bins[highIndex].priceHigh,
|
|
751
|
+
volume: valueVolume
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function clampIndex(value, count) {
|
|
756
|
+
return Math.max(0, Math.min(count - 1, value));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/analytics/volume-profile-overlay.ts
|
|
760
|
+
var DEFAULT_COLORS = {
|
|
761
|
+
bar: "#2962ff",
|
|
762
|
+
valueArea: "#2962ff",
|
|
763
|
+
poc: "#f0b90b"
|
|
764
|
+
};
|
|
765
|
+
var overlayCounter = 0;
|
|
766
|
+
function addVolumeProfileOverlay(context, chart, bars, options = {}) {
|
|
767
|
+
const profile = computeVolumeProfile(context, bars, options);
|
|
768
|
+
overlayCounter += 1;
|
|
769
|
+
const id = options.id ?? `com.wicklib.volume-profile-${overlayCounter}`;
|
|
770
|
+
const side = options.side ?? "left";
|
|
771
|
+
const widthRatio = clamp(options.widthRatio ?? 0.22, 0.02, 0.9);
|
|
772
|
+
const colors = { ...DEFAULT_COLORS, ...options.colors };
|
|
773
|
+
const maxVolume = Math.max(0, ...profile.bins.map((bin) => bin.volume));
|
|
774
|
+
const valueArea = profile.valueArea;
|
|
775
|
+
const overlay = chart.addOverlay({
|
|
776
|
+
id,
|
|
777
|
+
...options.paneId ? { paneId: options.paneId } : {},
|
|
778
|
+
draw(canvas, frame) {
|
|
779
|
+
if (maxVolume <= 0) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const maxWidth = frame.plot.width * widthRatio;
|
|
783
|
+
for (const bin of profile.bins) {
|
|
784
|
+
if (bin.volume <= 0) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const top = frame.priceToY(bin.priceHigh);
|
|
788
|
+
const bottom = frame.priceToY(bin.priceLow);
|
|
789
|
+
const height = Math.max(1, Math.abs(bottom - top));
|
|
790
|
+
const width = bin.volume / maxVolume * maxWidth;
|
|
791
|
+
const x = side === "left" ? frame.plot.left : frame.plot.right - width;
|
|
792
|
+
const inValueArea = valueArea !== null && bin.priceMid >= valueArea.low && bin.priceMid <= valueArea.high;
|
|
793
|
+
const isPoc = profile.poc?.index === bin.index;
|
|
794
|
+
canvas.globalAlpha = isPoc ? 0.55 : inValueArea ? 0.4 : 0.22;
|
|
795
|
+
canvas.fillStyle = isPoc ? colors.poc : inValueArea ? colors.valueArea : colors.bar;
|
|
796
|
+
canvas.fillRect(x, Math.min(top, bottom), width, height);
|
|
797
|
+
}
|
|
798
|
+
canvas.globalAlpha = 1;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
return {
|
|
802
|
+
id: overlay.id,
|
|
803
|
+
paneId: overlay.paneId,
|
|
804
|
+
requestRender: overlay.requestRender,
|
|
805
|
+
remove: overlay.remove,
|
|
806
|
+
profile
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function clamp(value, min, max) {
|
|
810
|
+
return Math.min(max, Math.max(min, value));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/features.ts
|
|
814
|
+
var PRO_FEATURES = [
|
|
815
|
+
"orderflow",
|
|
816
|
+
"volume-profile",
|
|
817
|
+
"panes",
|
|
818
|
+
"layouts",
|
|
819
|
+
"strategy",
|
|
820
|
+
"alerts",
|
|
821
|
+
"script-functions"
|
|
822
|
+
];
|
|
823
|
+
var PRO_CORE_FEATURES = PRO_FEATURES;
|
|
824
|
+
|
|
825
|
+
// src/extensions.ts
|
|
826
|
+
var NAMES = {
|
|
827
|
+
orderflow: "Orderflow",
|
|
828
|
+
"volume-profile": "Volume Profile",
|
|
829
|
+
panes: "Panes",
|
|
830
|
+
layouts: "Layouts",
|
|
831
|
+
strategy: "Strategy",
|
|
832
|
+
alerts: "Alerts",
|
|
833
|
+
"script-functions": "Script Functions"
|
|
834
|
+
};
|
|
835
|
+
var PRO_EXTENSIONS = Object.freeze(
|
|
836
|
+
[
|
|
837
|
+
"orderflow",
|
|
838
|
+
"volume-profile",
|
|
839
|
+
"panes",
|
|
840
|
+
"layouts",
|
|
841
|
+
"strategy",
|
|
842
|
+
"alerts",
|
|
843
|
+
"script-functions"
|
|
844
|
+
].map(
|
|
845
|
+
(feature) => Object.freeze({
|
|
846
|
+
kind: "feature",
|
|
847
|
+
feature,
|
|
848
|
+
meta: Object.freeze({
|
|
849
|
+
id: `com.wicklib.${feature}`,
|
|
850
|
+
name: NAMES[feature],
|
|
851
|
+
version: PRO_VERSION,
|
|
852
|
+
license: "pro"
|
|
853
|
+
})
|
|
854
|
+
})
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
function getProExtensions(context) {
|
|
858
|
+
return PRO_EXTENSIONS.filter((extension) => context.hasFeature(extension.feature));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/license/verify.ts
|
|
862
|
+
var LicenseError = class extends Error {
|
|
863
|
+
code;
|
|
864
|
+
constructor(code, message) {
|
|
865
|
+
super(message);
|
|
866
|
+
this.name = "LicenseError";
|
|
867
|
+
this.code = code;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
async function createLicenseContext(token, options) {
|
|
871
|
+
const parts = token.split(".");
|
|
872
|
+
if (parts.length !== 3) {
|
|
873
|
+
throw new LicenseError("INVALID_TOKEN", "License token must have three segments");
|
|
874
|
+
}
|
|
875
|
+
const [headerPart, payloadPart, signaturePart] = parts;
|
|
876
|
+
const header = decodeJson(headerPart, "INVALID_TOKEN");
|
|
877
|
+
if (header.alg !== "Ed25519") {
|
|
878
|
+
throw new LicenseError(
|
|
879
|
+
"UNSUPPORTED_ALGORITHM",
|
|
880
|
+
`Unsupported license algorithm: ${header.alg}`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const payload = decodeJson(payloadPart, "INVALID_PAYLOAD");
|
|
884
|
+
validatePayload(payload);
|
|
885
|
+
if (header.kid && header.kid !== payload.kid) {
|
|
886
|
+
throw new LicenseError("INVALID_PAYLOAD", "License header and payload kid do not match");
|
|
887
|
+
}
|
|
888
|
+
const publicKey = options.publicKeys[payload.kid];
|
|
889
|
+
if (!publicKey) {
|
|
890
|
+
throw new LicenseError("UNKNOWN_KID", `Unknown license key id: ${payload.kid}`);
|
|
891
|
+
}
|
|
892
|
+
const product = options.product ?? "wicklib-pro";
|
|
893
|
+
if (payload.product !== product) {
|
|
894
|
+
throw new LicenseError(
|
|
895
|
+
"WRONG_PRODUCT",
|
|
896
|
+
`License is for ${payload.product}, expected ${product}`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
const now = options.now?.() ?? Math.floor(Date.now() / 1e3);
|
|
900
|
+
if (payload.exp <= now) {
|
|
901
|
+
throw new LicenseError("EXPIRED", "License has expired");
|
|
902
|
+
}
|
|
903
|
+
if (payload.domains.length > 0) {
|
|
904
|
+
if (!options.domain) {
|
|
905
|
+
throw new LicenseError("DOMAIN_REQUIRED", "This license requires a domain");
|
|
906
|
+
}
|
|
907
|
+
if (!domainAllowed(payload.domains, options.domain)) {
|
|
908
|
+
throw new LicenseError(
|
|
909
|
+
"DOMAIN_NOT_ALLOWED",
|
|
910
|
+
`Domain is not covered by the license: ${options.domain}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const subtle = globalThis.crypto?.subtle;
|
|
915
|
+
if (!subtle) {
|
|
916
|
+
throw new LicenseError("CRYPTO_UNAVAILABLE", "Web Crypto is not available");
|
|
917
|
+
}
|
|
918
|
+
let key;
|
|
919
|
+
try {
|
|
920
|
+
const raw = base64UrlDecode(publicKey);
|
|
921
|
+
key = await subtle.importKey("raw", raw, { name: "Ed25519" }, false, ["verify"]);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
throw new LicenseError(
|
|
924
|
+
"INVALID_PAYLOAD",
|
|
925
|
+
error instanceof Error ? `Invalid license public key: ${error.message}` : "Invalid license public key"
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
let valid = false;
|
|
929
|
+
try {
|
|
930
|
+
valid = await subtle.verify(
|
|
931
|
+
{ name: "Ed25519" },
|
|
932
|
+
key,
|
|
933
|
+
base64UrlDecode(signaturePart),
|
|
934
|
+
new TextEncoder().encode(`${headerPart}.${payloadPart}`)
|
|
935
|
+
);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
throw new LicenseError(
|
|
938
|
+
"SIGNATURE_INVALID",
|
|
939
|
+
error instanceof Error ? `Signature verification failed: ${error.message}` : "Signature verification failed"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (!valid) {
|
|
943
|
+
throw new LicenseError("SIGNATURE_INVALID", "License signature is invalid");
|
|
944
|
+
}
|
|
945
|
+
const granted = new Set(payload.features);
|
|
946
|
+
return { hasFeature: (feature) => granted.has(feature) };
|
|
947
|
+
}
|
|
948
|
+
function validatePayload(payload) {
|
|
949
|
+
if (!payload || typeof payload.product !== "string" || typeof payload.plan !== "string" || typeof payload.kid !== "string" || payload.kid.length === 0 || !Number.isFinite(payload.exp) || !Number.isInteger(payload.seats) || payload.seats < 1 || !Array.isArray(payload.domains) || payload.domains.some((domain) => typeof domain !== "string") || !Array.isArray(payload.features) || payload.features.some((feature) => typeof feature !== "string")) {
|
|
950
|
+
throw new LicenseError("INVALID_PAYLOAD", "License payload is invalid");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function domainAllowed(domains, host) {
|
|
954
|
+
const normalized = normalizeDomain(host);
|
|
955
|
+
return domains.some(
|
|
956
|
+
(domain) => domain === "*" || normalizeDomain(domain) === normalized
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
function normalizeDomain(value) {
|
|
960
|
+
try {
|
|
961
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
962
|
+
return url.hostname.toLowerCase();
|
|
963
|
+
} catch {
|
|
964
|
+
return value.toLowerCase();
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function decodeJson(value, code) {
|
|
968
|
+
try {
|
|
969
|
+
return JSON.parse(new TextDecoder().decode(base64UrlDecode(value)));
|
|
970
|
+
} catch {
|
|
971
|
+
throw new LicenseError(code, "License token contains invalid base64url or JSON");
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function base64UrlDecode(value) {
|
|
975
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
976
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
977
|
+
try {
|
|
978
|
+
const binary = atob(padded);
|
|
979
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
980
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
981
|
+
bytes[index] = binary.charCodeAt(index);
|
|
982
|
+
}
|
|
983
|
+
return bytes;
|
|
984
|
+
} catch {
|
|
985
|
+
throw new LicenseError("INVALID_TOKEN", "License token contains invalid base64url");
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/index.ts
|
|
990
|
+
var import_core8 = require("@wicklib/core");
|
|
991
|
+
function hasProFeature(context, feature) {
|
|
992
|
+
return context.hasFeature(feature);
|
|
993
|
+
}
|
|
994
|
+
function assertProFeature(context, feature) {
|
|
995
|
+
(0, import_core8.assertFeature)(context, feature);
|
|
996
|
+
}
|
|
997
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
998
|
+
0 && (module.exports = {
|
|
999
|
+
AlertEngine,
|
|
1000
|
+
DepthAggregator,
|
|
1001
|
+
FootprintAggregator,
|
|
1002
|
+
LicenseError,
|
|
1003
|
+
PRO_CORE_FEATURES,
|
|
1004
|
+
PRO_EXTENSIONS,
|
|
1005
|
+
PRO_FEATURES,
|
|
1006
|
+
PRO_VERSION,
|
|
1007
|
+
addVolumeProfileOverlay,
|
|
1008
|
+
assertProFeature,
|
|
1009
|
+
computeDepth,
|
|
1010
|
+
computeFootprint,
|
|
1011
|
+
computeVolumeProfile,
|
|
1012
|
+
createLicenseContext,
|
|
1013
|
+
detectAbsorption,
|
|
1014
|
+
evaluateAlerts,
|
|
1015
|
+
getProExtensions,
|
|
1016
|
+
hasProFeature,
|
|
1017
|
+
runBacktest,
|
|
1018
|
+
runScriptBacktest
|
|
1019
|
+
});
|
|
1020
|
+
//# sourceMappingURL=index.cjs.map
|