@steerprotocol/strategy-utils 3.1.2 → 3.2.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.
Files changed (36) hide show
  1. package/.github/workflows/nodejs.yml +12 -12
  2. package/.vscode/settings.json +6 -0
  3. package/CHANGELOG.md +3 -45
  4. package/asconfig.json +11 -9
  5. package/assembly/index.ts +1 -0
  6. package/assembly/panoptic/host.ts +285 -0
  7. package/assembly/panoptic/index.ts +3 -0
  8. package/assembly/panoptic/methods.ts +64 -0
  9. package/assembly/panoptic/types.ts +911 -0
  10. package/assembly/utils/Math.ts +12 -17
  11. package/assembly/utils/MovingAverages.ts +26 -19
  12. package/assembly/utils/Ranges.ts +15 -15
  13. package/assembly/utils/UniswapLiquidityUtils.ts +48 -0
  14. package/assembly/utils/index.ts +1 -3
  15. package/assembly/utils/types/Position.ts +0 -3
  16. package/assembly/utils/types/Price.ts +54 -0
  17. package/assembly/utils/types/index.ts +1 -4
  18. package/index.js +3 -14
  19. package/package.json +10 -14
  20. package/scripts/build-docs.js +68 -0
  21. package/tests/fixtures/json-compat.ts +30 -0
  22. package/tests/fixtures/panoptic-consumer.ts +24 -0
  23. package/tests/index.test.ts +55 -227
  24. package/assembly/utils/CandleGenerator.ts +0 -60
  25. package/assembly/utils/MarketFeedAggregator.ts +0 -140
  26. package/assembly/utils/SlidingWindow.ts +0 -59
  27. package/assembly/utils/env.ts +0 -23
  28. package/assembly/utils/triggers.ts +0 -585
  29. package/assembly/utils/types/Candle.ts +0 -39
  30. package/assembly/utils/types/DataConnectorConfig.ts +0 -7
  31. package/assembly/utils/types/ExecutionContext.ts +0 -11
  32. package/assembly/utils/types/RawTradeData.ts +0 -14
  33. package/index.html +0 -10
  34. package/readme.md +0 -387
  35. package/tests/debug.wasm +0 -0
  36. package/tests/utils.ts +0 -607
@@ -1,238 +1,66 @@
1
- import { candles2, } from "./utils";
2
- import fs from 'fs';
3
- import { WasmModule, loadWasm } from "@steerprotocol/app-loader";
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const { execFileSync } = require('child_process');
4
+ const loader = require('@assemblyscript/loader');
5
+ const path = require('path');
4
6
 
5
-
6
- describe("Unit tests", () => {
7
- let myModule: WasmModule;
8
-
9
- beforeEach(async () => {
10
- myModule = await loadWasm(fs.readFileSync(__dirname + "/debug.wasm"), {})
11
- });
12
-
13
- describe("Trigger tests", () => {
14
- // Testing the following options
15
- // 'Current Price set distance from center of positions',
16
- // 'Price leaves active range',
17
- // 'Price moves percentage of active range away',
18
- // 'Price moves one way past positions',
19
- // 'None'
20
- // TEST tirggers
21
- test("Can return positions on none type", async () => {
22
- const config = `{
23
- "triggerStyle": "None",
24
- "period":6,
25
- "standardDeviations":2.0,
26
- "liquidityShape": "Linear",
27
- "poolFee": 3000,
28
- "placementType": "Position over current price",
29
- "positionSize": 600
30
- }`
31
- myModule.initialize(config);
32
- const positions = '[[257100],[257300],[1]]'
33
- const currentTick = '257301'
34
- const timeSinceLastExecution = '5600'
35
- const result = myModule.execute
36
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
37
- expect(result).not.toEqual('continue')
38
- });
39
-
40
- test
41
- ("Can return continue for active trigger", async () => {
42
- const config = `{
43
- "elapsedTendTime": 604800,
44
- "triggerStyle": "Price leaves active range",
45
- "strategy": "Classic",
46
- "liquidityShape": "Linear",
47
- "poolFee": 500,
48
- "period":6,
49
- "standardDeviations":2.0
50
- }`
51
- myModule.initialize(config);
52
- const positions = '[[257100],[257300],[1]]'
53
- const currentTick = '257251'
54
- const timeSinceLastExecution = '5600'
55
- const result = myModule.execute(//...[JSON.stringify(candles2), positions])
56
- JSON.stringify(candles2),
57
- positions,
58
- currentTick,
59
- timeSinceLastExecution);
60
- expect(result).toEqual('continue')
61
- });
62
-
63
- test("Can return positions for active trigger", async () => {
64
- const config = `{
65
- "elapsedTendTime": 604800,
66
- "triggerStyle": "Price leaves active range",
67
- "strategy": "Classic",
68
- "liquidityShape": "Linear",
69
- "poolFee": 3000,
70
- "placementType": "Position over current price",
71
- "period":6,
72
- "standardDeviations":2.0,
73
- "positionSize": 600
74
- }`
75
- myModule.initialize(config);
76
- const positions = '[[257100],[257300],[1]]'
77
- const currentTick = '257301'
78
- const timeSinceLastExecution = '5600'
79
- const result = myModule.execute
80
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
81
- expect(result).not.toEqual('continue')
7
+ describe('AssemblyScript package surface', () => {
8
+ it('compiles the package with the modern AssemblyScript toolchain', () => {
9
+ execFileSync('npm', ['run', 'asbuild'], {
10
+ cwd: path.resolve(__dirname, '..'),
11
+ stdio: 'pipe',
12
+ });
82
13
  });
83
14
 
84
- test("Can return continue for distance trigger", async () => {
85
- const config = `{
86
- "elapsedTendTime": 604800,
87
- "triggerStyle": "Current Price set distance from center of positions",
88
- "tickDistanceFromCenter": 100,
89
- "strategy": "Classic",
90
- "liquidityShape": "Linear",
91
- "poolFee": 3000,
92
- "placementType": "Position over current price",
93
- "period":6,
94
- "standardDeviations":2.0,
95
- "positionSize": 600
96
- }`
97
- myModule.initialize(config);
98
- const positions = '[[257100],[257300],[1]]'
99
- const currentTick = '257251'
100
- const timeSinceLastExecution = '5600'
101
- const result = myModule.execute
102
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
103
- expect(result).toEqual('continue')
15
+ it('compiles a Panoptic consumer using exported DTOs and wrappers', () => {
16
+ execFileSync(
17
+ path.resolve(__dirname, '../node_modules/.bin/asc'),
18
+ [path.resolve(__dirname, 'fixtures/panoptic-consumer.ts'), '--config', path.resolve(__dirname, '../asconfig.json'), '--noEmit'],
19
+ {
20
+ cwd: path.resolve(__dirname, '..'),
21
+ stdio: 'pipe',
22
+ },
23
+ );
104
24
  });
105
25
 
106
- test("Can return positions for distance trigger", async () => {
107
- const config = `{
108
- "elapsedTendTime": 604800,
109
- "triggerStyle": "Current Price set distance from center of positions",
110
- "tickDistanceFromCenter": 100,
111
- "strategy": "Classic",
112
- "liquidityShape": "Linear",
113
- "poolFee": 3000,
114
- "placementType": "Position over current price",
115
- "period":6,
116
- "standardDeviations":2.0,
117
- "positionSize": 600
118
- }`
119
- myModule.initialize(config);
120
- const positions = '[[257100],[257300],[1]]'
121
- const currentTick = '257301'
122
- const timeSinceLastExecution = '5600'
123
- const result = myModule.execute
124
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
125
- expect(result).not.toEqual('continue')
126
- });
26
+ it('preserves json-as runtime compatibility for legacy prices and dynamic Merkle proof keys', () => {
27
+ const repoRoot = path.resolve(__dirname, '..');
28
+ const wasmPath = path.join(os.tmpdir(), 'strategy-utils-json-compat.fixture.wasm');
127
29
 
128
- test("Can return continue for percentage trigger", async () => {
129
- const config = `{
130
- "elapsedTendTime": 604800,
131
- "triggerStyle": "Price moves percentage of active range away",
132
- "percentageOfPositionRangeToTrigger": 1,
133
- "strategy": "Classic",
134
- "liquidityShape": "Linear",
135
- "poolFee": 3000,
136
- "placementType": "Position over current price",
137
- "period":6,
138
- "standardDeviations":2.0,
139
- "positionSize": 600
140
- }`
141
- myModule.initialize(config);
142
- const positions = '[[257100],[257300],[1]]'
143
- const currentTick = '257251'
144
- const timeSinceLastExecution = '5600'
145
- const result = myModule.execute
146
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
147
- expect(result).toEqual('continue')
148
- });
30
+ execFileSync(
31
+ path.resolve(repoRoot, 'node_modules/.bin/asc'),
32
+ [
33
+ path.resolve(__dirname, 'fixtures/json-compat.ts'),
34
+ '--config',
35
+ path.resolve(repoRoot, 'asconfig.json'),
36
+ '--outFile',
37
+ wasmPath,
38
+ '--exportRuntime',
39
+ ],
40
+ {
41
+ cwd: repoRoot,
42
+ stdio: 'pipe',
43
+ },
44
+ );
149
45
 
150
- test("Can return positions for percentage trigger", async () => {
151
- const config = `{
152
- "elapsedTendTime": 604800,
153
- "triggerStyle": "Price moves percentage of active range away",
154
- "percentageOfPositionRangeToTrigger": 1,
155
- "strategy": "Classic",
156
- "liquidityShape": "Linear",
157
- "poolFee": 3000,
158
- "placementType": "Position over current price",
159
- "period":6,
160
- "standardDeviations":2.0,
161
- "positionSize": 600
162
- }`
163
- myModule.initialize(config);
164
- const positions = '[[257100],[257300],[1]]'
165
- const currentTick = '257301'
166
- const timeSinceLastExecution = '5600'
167
- const result = myModule.execute
168
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
169
- expect(result).not.toEqual('continue')
170
- });
46
+ const wasm = loader.instantiateSync(fs.readFileSync(wasmPath), {});
47
+ const { parseStringifiedPrices, roundTripMerkleProofActions, __getString, __newString } = wasm.exports;
171
48
 
172
- test("Can return continue for one way inactive - active", async () => {
173
- const config = `{
174
- "elapsedTendTime": 604800,
175
- "triggerStyle": "Price moves one way past positions",
176
- "triggerWhenOver": true,
177
- "poolFee": 3000,
178
- "placementType": "Position over current price",
179
- "period":6,
180
- "standardDeviations":2.0,
181
- "positionSize": 600
182
- }`
183
- myModule.initialize(config);
184
- const positions = '[[257100],[257300],[1]]'
185
- const currentTick = '257251'
186
- const timeSinceLastExecution = '5600'
187
- const result = myModule.execute
188
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
189
- expect(result).toEqual('continue')
190
- });
49
+ expect(__getString(parseStringifiedPrices())).toBe('ok');
191
50
 
192
- test("Can return continue for one way inactive - below", async () => {
193
- const config = `{
194
- "elapsedTendTime": 604800,
195
- "triggerStyle": "Price moves one way past positions",
196
- "triggerWhenOver": true,
197
- "strategy": "Classic",
198
- "liquidityShape": "Linear",
199
- "poolFee": 3000,
200
- "placementType": "Position over current price",
201
- "triggerWhenOver": true,
202
- "period":6,
203
- "standardDeviations":2.0,
204
- "positionSize": 600
205
- }`
206
- myModule.initialize(config);
207
- const positions = '[[257100],[257300],[1]]'
208
- const currentTick = '257000'
209
- const timeSinceLastExecution = '5600'
210
- const result = myModule.execute
211
- (JSON.stringify(candles2), positions, currentTick, timeSinceLastExecution);
212
- expect(result).toEqual('continue')
213
- });
51
+ const input = JSON.stringify({
52
+ leafs: [],
53
+ proofsByAction: {
54
+ approveToken: ['0xaaa'],
55
+ rebalanceVault: ['0xbbb', '0xccc'],
56
+ },
57
+ proofsByDigest: {
58
+ '0x111': ['0xddd'],
59
+ },
60
+ });
214
61
 
215
- test("Can return positions for one way inactive - above", async () => {
216
- const config = `{
217
- "elapsedTendTime": 604800,
218
- "triggerStyle": "Price moves one way past positions",
219
- "triggerWhenOver": true,
220
- "strategy": "Bollinger Band",
221
- "liquidityShape": "Linear",
222
- "poolFee": 500,
223
- "placementType": "Position over current price",
224
- "triggerWhenOver": true,
225
- "lookback": 12,
226
- "period":6,
227
- "standardDeviations":2.0,
228
- }`
229
- myModule.initialize(config);
230
- const positions = '[[257100],[257300],[1]]'
231
- const currentTick = '257301'
232
- const timeSinceLastExecution = '5600'
233
- const result = myModule.execute
234
- (JSON.stringify([...candles2]), positions, currentTick, timeSinceLastExecution);
235
- expect(result).not.toEqual('continue')
62
+ const output = JSON.parse(__getString(roundTripMerkleProofActions(__newString(input))));
63
+ expect(output.approveToken).toStrictEqual(['0xaaa']);
64
+ expect(output.rebalanceVault).toStrictEqual(['0xbbb', '0xccc']);
236
65
  });
237
- });
238
- });
66
+ });
@@ -1,60 +0,0 @@
1
- import { Candle } from "./types/Candle";
2
- import { RawTradeData } from "./types/RawTradeData";
3
-
4
- // CandlestickConverter class provides a static method to convert raw trade data into OHLCV format
5
- export class CandlestickConverter {
6
- // convertToOHLCV method accepts raw trade data and a period (default is 3600 seconds or 1 hour)
7
- // and returns the data in OHLCV format
8
- static convertToOHLCV(rawData: RawTradeData[], period: i32 = 3600): Candle[] {
9
- // If the raw data is empty, an error is thrown
10
- if (rawData.length === 0) {
11
- throw new Error("Input data is empty");
12
- }
13
-
14
- // Array to hold the converted data
15
- let ohlcvData: Candle[] = [];
16
-
17
- // Initialize variables
18
- let i: i32 = 0;
19
- let currentTimestamp: i32 = Math.floor(rawData[0].timestamp / period) * period;
20
- let open: f64 = rawData[0].price;
21
- let high: f64 = rawData[0].price;
22
- let low: f64 = rawData[0].price;
23
- let close: f64 = rawData[0].price;
24
- let volume: f64 = 0.0;
25
-
26
- // Loop through all raw data
27
- while (i < rawData.length) {
28
- // For each period, set the initial values of open, high, low to the closing price of the last period
29
- // and reset the volume to 0
30
- open = close;
31
- high = close;
32
- low = close;
33
- volume = 0.0;
34
-
35
- // Process all raw data within the current period
36
- while (i < rawData.length && rawData[i].timestamp < currentTimestamp + period) {
37
- // Set the open price to the price of the first trade in the period
38
- open = i == 0 ? rawData[i].price : open;
39
- // Update high and low prices
40
- high = Math.max(high, rawData[i].price);
41
- low = Math.min(low, rawData[i].price);
42
- // Update close price to the price of the last trade in the period
43
- close = rawData[i].price;
44
- // Update the total volume
45
- volume += rawData[i].volume;
46
- i++;
47
- }
48
-
49
- // Create a new Candle object with the calculated OHLCV values and add it to the array
50
- let ohlcv: Candle = new Candle(currentTimestamp, open, high, low, close, volume);
51
- ohlcvData.push(ohlcv);
52
-
53
- // Move to the next period
54
- currentTimestamp += period;
55
- }
56
-
57
- // Return the converted data
58
- return ohlcvData;
59
- }
60
- }
@@ -1,140 +0,0 @@
1
- class Candle {
2
- timestamp: f64;
3
- open: f64;
4
- high: f64;
5
- close: f64;
6
- low: f64;
7
- volume: f64;
8
-
9
- constructor(
10
- timestamp: f64,
11
- open: f64,
12
- high: f64,
13
- close: f64,
14
- low: f64,
15
- volume: f64
16
- ) {
17
- this.timestamp = timestamp;
18
- this.open = open;
19
- this.high = high;
20
- this.close = close;
21
- this.low = low;
22
- this.volume = volume;
23
- }
24
- }
25
-
26
- class MarketFeedAggregator {
27
- data: Array<Candle>;
28
-
29
- constructor(data: Array<Candle>) {
30
- this.data = data;
31
- }
32
-
33
- geometricMean(): Candle {
34
- let n: f64 = f64(this.data.length);
35
- let logSum = new Candle(0, 0, 0, 0, 0, 0);
36
-
37
- for (let i = 0; i < this.data.length; i++) {
38
- let candle = this.data[i];
39
- logSum.timestamp += Mathf.log(candle.timestamp);
40
- logSum.open += Mathf.log(candle.open);
41
- logSum.high += Mathf.log(candle.high);
42
- logSum.close += Mathf.log(candle.close);
43
- logSum.low += Mathf.log(candle.low);
44
- logSum.volume += Mathf.log(candle.volume);
45
- }
46
-
47
- return new Candle(
48
- Mathf.exp(logSum.timestamp / n),
49
- Mathf.exp(logSum.open / n),
50
- Mathf.exp(logSum.high / n),
51
- Mathf.exp(logSum.close / n),
52
- Mathf.exp(logSum.low / n),
53
- Mathf.exp(logSum.volume / n)
54
- );
55
- }
56
-
57
- timeWeightedAveragePrice(): f64 {
58
- let sumProduct: f64 = 0;
59
- let sumTime: f64 = 0;
60
-
61
- for (let i = 0; i < this.data.length; i++) {
62
- let candle = this.data[i];
63
- let midPrice: f64 = (candle.high + candle.low) / 2;
64
- sumProduct += midPrice * candle.timestamp;
65
- sumTime += candle.timestamp;
66
- }
67
-
68
- return sumProduct / sumTime;
69
- }
70
-
71
- volumeWeightedAveragePrice(): f64 {
72
- let sumProduct: f64 = 0;
73
- let sumVolume: f64 = 0;
74
-
75
- for (let i = 0; i < this.data.length; i++) {
76
- let candle = this.data[i];
77
- let midPrice: f64 = (candle.high + candle.low) / 2;
78
- sumProduct += midPrice * candle.volume;
79
- sumVolume += candle.volume;
80
- }
81
-
82
- return sumProduct / sumVolume;
83
- }
84
-
85
- weightedMean(weights: Candle): Candle {
86
- let sumWeights: f64 = 0;
87
- let sumProduct: Candle = new Candle(0, 0, 0, 0, 0, 0);
88
-
89
- for (let i = 0; i < this.data.length; i++) {
90
- let candle = this.data[i];
91
- sumProduct.timestamp += weights.timestamp * candle.timestamp;
92
- sumProduct.open += weights.open * candle.open;
93
- sumProduct.high += weights.high * candle.high;
94
- sumProduct.close += weights.close * candle.close;
95
- sumProduct.low += weights.low * candle.low;
96
- sumProduct.volume += weights.volume * candle.volume;
97
-
98
- sumWeights +=
99
- weights.timestamp +
100
- weights.open +
101
- weights.high +
102
- weights.close +
103
- weights.low +
104
- weights.volume;
105
- }
106
-
107
- return new Candle(
108
- sumProduct.timestamp / sumWeights,
109
- sumProduct.open / sumWeights,
110
- sumProduct.high / sumWeights,
111
- sumProduct.close / sumWeights,
112
- sumProduct.low / sumWeights,
113
- sumProduct.volume / sumWeights
114
- );
115
- }
116
-
117
- arithmeticMean(): Candle {
118
- let n: f64 = f64(this.data.length);
119
- let sum: Candle = new Candle(0, 0, 0, 0, 0, 0);
120
-
121
- for (let i = 0; i < this.data.length; i++) {
122
- let candle = this.data[i];
123
- sum.timestamp += candle.timestamp;
124
- sum.open += candle.open;
125
- sum.high += candle.high;
126
- sum.close += candle.close;
127
- sum.low += candle.low;
128
- sum.volume += candle.volume;
129
- }
130
-
131
- return new Candle(
132
- sum.timestamp / n,
133
- sum.open / n,
134
- sum.high / n,
135
- sum.close / n,
136
- sum.low / n,
137
- sum.volume / n
138
- );
139
- }
140
- }
@@ -1,59 +0,0 @@
1
- export class SlidingWindow<T> {
2
- private data: Array<T>;
3
- private windowSize: i32;
4
- private formula: (window: Array<T>) => T;
5
- private cursor: i32;
6
-
7
- constructor(windowSize: i32, formula: (window: Array<T>) => T) {
8
- if (windowSize < 1) {
9
- throw new Error("windowSize must be greater than 0");
10
- }
11
- if (formula === null) {
12
- throw new Error("formula function must be provided");
13
- }
14
- this.windowSize = windowSize;
15
- this.formula = formula;
16
- this.data = new Array<T>(windowSize);
17
- this.cursor = 0;
18
- }
19
-
20
- addValue(value: T): void {
21
- this.data[this.cursor] = value;
22
- this.cursor = (this.cursor + 1) % this.windowSize;
23
- }
24
-
25
- getLastValue(): T {
26
- let index = this.cursor === 0 ? this.windowSize - 1 : this.cursor - 1;
27
- return this.data[index];
28
- }
29
-
30
- clear(): void {
31
- this.data.fill(null);
32
- }
33
-
34
- setWindowSize(size: i32): void {
35
- if (size < 1) {
36
- throw new Error("windowSize must be greater than 0");
37
- }
38
- this.windowSize = size;
39
- this.data = new Array<T>(size);
40
- }
41
-
42
- getWindow(): Array<T> {
43
- let result = new Array<T>(this.windowSize);
44
- for (let i = 0; i < this.windowSize; i++) {
45
- let index = (this.cursor + i) % this.windowSize;
46
- result[i] = this.data[index];
47
- }
48
- return result;
49
- }
50
-
51
- getFormulaResult(): T {
52
- let window = this.getWindow();
53
- return this.formula(window);
54
- }
55
-
56
- isStable(): bool {
57
- return this.data.length >= this.windowSize;
58
- }
59
- }
@@ -1,23 +0,0 @@
1
- export declare function generateCandles(data: string, candleSize: string): string;
2
- @external("env", "ccxt_fetchOHLCV")
3
- declare function _ccxt_fetchOHLCV(exchangeId: string, symbol: string, timeframe: string, limit: number, since: number): StaticArray<StaticArray<f64>>;
4
-
5
- // Required for asyncify
6
- // @ts-ignore: Global should exist here
7
- @global let __ASYNCIFY_INITIALIZED = false;
8
- @external("env", "_initAsyncify")
9
- declare function _initAsyncify(asyncify_data_ptr: usize, stack_pointer: usize): void;
10
-
11
- export function ccxt_fetchOHLCV(exchangeId: string, symbol: string, timeframe: string, limit: number, since: number): StaticArray<StaticArray<f64>> {
12
- if (!__ASYNCIFY_INITIALIZED) {
13
- // We need to initialize space for Asyncify to work.
14
- // Asyncify will create a full - duplex communication channel through this bit of memory.
15
- // For every asyncify-enabled function, make sure to add this
16
- // memory.data() reserves a section of data that is not touched by the Garbage Collector
17
- // We can only do this once or else we will cause a memory leak and eventual overflow
18
- // It will not grow past the stack pointer which is where real data starts.
19
- _initAsyncify(memory.data(8, 16), __stack_pointer);
20
- __ASYNCIFY_INITIALIZED = true;
21
- }
22
- return _ccxt_fetchOHLCV(exchangeId, symbol, timeframe, limit, since);
23
- }