openbroker 1.9.2 → 1.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +16 -0
- package/SKILL.md +69 -3
- package/dist/auto/cli.js +4 -1
- package/dist/auto/examples/dca.d.ts +2 -1
- package/dist/auto/examples/dca.d.ts.map +1 -1
- package/dist/auto/examples/dca.js +19 -1
- package/dist/auto/examples/funding-arb.d.ts +2 -1
- package/dist/auto/examples/funding-arb.d.ts.map +1 -1
- package/dist/auto/examples/funding-arb.js +19 -2
- package/dist/auto/examples/grid.d.ts +2 -1
- package/dist/auto/examples/grid.d.ts.map +1 -1
- package/dist/auto/examples/grid.js +18 -2
- package/dist/auto/examples/mm-maker.d.ts +2 -1
- package/dist/auto/examples/mm-maker.d.ts.map +1 -1
- package/dist/auto/examples/mm-maker.js +18 -2
- package/dist/auto/examples/mm-spread.d.ts +2 -1
- package/dist/auto/examples/mm-spread.d.ts.map +1 -1
- package/dist/auto/examples/mm-spread.js +18 -2
- package/dist/auto/examples/price-alert.d.ts +2 -1
- package/dist/auto/examples/price-alert.d.ts.map +1 -1
- package/dist/auto/examples/price-alert.js +2 -1
- package/dist/auto/guardrails.d.ts +19 -0
- package/dist/auto/guardrails.d.ts.map +1 -0
- package/dist/auto/guardrails.js +575 -0
- package/dist/auto/guardrails.test.d.ts +2 -0
- package/dist/auto/guardrails.test.d.ts.map +1 -0
- package/dist/auto/guardrails.test.js +173 -0
- package/dist/auto/loader.d.ts +3 -3
- package/dist/auto/loader.d.ts.map +1 -1
- package/dist/auto/loader.js +25 -3
- package/dist/auto/realtime.d.ts +45 -0
- package/dist/auto/realtime.d.ts.map +1 -0
- package/dist/auto/realtime.js +177 -0
- package/dist/auto/realtime.test.d.ts +2 -0
- package/dist/auto/realtime.test.d.ts.map +1 -0
- package/dist/auto/realtime.test.js +73 -0
- package/dist/auto/runtime.d.ts +3 -3
- package/dist/auto/runtime.d.ts.map +1 -1
- package/dist/auto/runtime.js +155 -65
- package/dist/auto/types.d.ts +45 -1
- package/dist/auto/types.d.ts.map +1 -1
- package/dist/core/client.d.ts +66 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +141 -4
- package/dist/core/ws.d.ts +14 -2
- package/dist/core/ws.d.ts.map +1 -1
- package/dist/core/ws.js +53 -7
- package/dist/lib.d.ts +2 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/package.json +5 -3
- package/scripts/auto/cli.ts +4 -1
- package/scripts/auto/examples/dca.ts +21 -2
- package/scripts/auto/examples/funding-arb.ts +21 -3
- package/scripts/auto/examples/grid.ts +20 -3
- package/scripts/auto/examples/mm-maker.ts +20 -3
- package/scripts/auto/examples/mm-spread.ts +20 -3
- package/scripts/auto/examples/price-alert.ts +4 -2
- package/scripts/auto/guardrails.test.ts +227 -0
- package/scripts/auto/guardrails.ts +700 -0
- package/scripts/auto/loader.ts +41 -4
- package/scripts/auto/realtime.test.ts +84 -0
- package/scripts/auto/realtime.ts +194 -0
- package/scripts/auto/runtime.ts +163 -69
- package/scripts/auto/types.ts +56 -1
- package/scripts/core/client.ts +175 -4
- package/scripts/core/ws.ts +57 -8
- package/scripts/lib.ts +10 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Funding Arbitrage — Collect funding by positioning opposite to the crowd
|
|
2
2
|
|
|
3
|
-
import type { AutomationAPI, AutomationConfig } from '../types.js';
|
|
3
|
+
import type { AutomationAPI, AutomationConfig, AutomationGuardrailContext, AutomationGuardrails } from '../types.js';
|
|
4
4
|
|
|
5
5
|
export const config: AutomationConfig = {
|
|
6
6
|
description: 'Funding arbitrage — collect funding by positioning opposite to the crowd',
|
|
@@ -13,6 +13,24 @@ export const config: AutomationConfig = {
|
|
|
13
13
|
},
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export function guardrails({ config: values }: AutomationGuardrailContext): AutomationGuardrails {
|
|
17
|
+
const sizeUsd = Number(values.sizeUsd ?? 5000);
|
|
18
|
+
return {
|
|
19
|
+
mode: 'trading',
|
|
20
|
+
allowedMarkets: [String(values.coin ?? 'HYPE')],
|
|
21
|
+
maxOrderUsd: sizeUsd * 1.1,
|
|
22
|
+
maxPositionUsd: sizeUsd * 1.1,
|
|
23
|
+
maxTotalExposureUsd: sizeUsd * 1.1,
|
|
24
|
+
maxLeverage: 1,
|
|
25
|
+
maxMarginUsedPct: 50,
|
|
26
|
+
maxOpenOrders: 5,
|
|
27
|
+
maxOrdersPerMinute: 4,
|
|
28
|
+
maxSlippageBps: 50,
|
|
29
|
+
allowMarketOrders: true,
|
|
30
|
+
allowAccountWideCancel: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
export default function fundingArb(api: AutomationAPI) {
|
|
17
35
|
const COIN = api.state.get<string>('coin', 'HYPE')!;
|
|
18
36
|
const SIZE_USD = api.state.get<number>('sizeUsd', 5000)!;
|
|
@@ -48,7 +66,7 @@ export default function fundingArb(api: AutomationAPI) {
|
|
|
48
66
|
if (shouldClose) {
|
|
49
67
|
api.log.info(`Funding dropped to ${annualizedPct.toFixed(2)}% (below ${CLOSE_AT}%), closing ${positionSide}`);
|
|
50
68
|
const closeIsBuy = positionSide === 'short';
|
|
51
|
-
await api.client.marketOrder(coin, closeIsBuy, positionSize);
|
|
69
|
+
await api.client.marketOrder(coin, closeIsBuy, positionSize, undefined, 1);
|
|
52
70
|
|
|
53
71
|
inPosition = false;
|
|
54
72
|
api.state.set('inPosition', false);
|
|
@@ -69,7 +87,7 @@ export default function fundingArb(api: AutomationAPI) {
|
|
|
69
87
|
const size = SIZE_USD / price;
|
|
70
88
|
|
|
71
89
|
api.log.info(`Funding at ${annualizedPct.toFixed(2)}% — opening ${side} ${size.toFixed(6)} ${coin}`);
|
|
72
|
-
const response = await api.client.marketOrder(coin, !shouldShort, size);
|
|
90
|
+
const response = await api.client.marketOrder(coin, !shouldShort, size, undefined, 1);
|
|
73
91
|
|
|
74
92
|
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
75
93
|
const status = response.response.data.statuses[0];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Grid Trading — Place buy/sell orders at evenly spaced price levels
|
|
2
2
|
|
|
3
|
-
import type { AutomationAPI, AutomationConfig } from '../types.js';
|
|
3
|
+
import type { AutomationAPI, AutomationConfig, AutomationGuardrailContext, AutomationGuardrails } from '../types.js';
|
|
4
4
|
|
|
5
5
|
export const config: AutomationConfig = {
|
|
6
6
|
description: 'Grid trading — buy/sell orders at evenly spaced price levels',
|
|
@@ -14,6 +14,23 @@ export const config: AutomationConfig = {
|
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
export function guardrails({ config: values }: AutomationGuardrailContext): AutomationGuardrails {
|
|
18
|
+
return {
|
|
19
|
+
mode: 'trading',
|
|
20
|
+
allowedMarkets: [String(values.coin ?? 'HYPE')],
|
|
21
|
+
maxOrderUsd: 10_000,
|
|
22
|
+
maxPositionUsd: 25_000,
|
|
23
|
+
maxTotalExposureUsd: 25_000,
|
|
24
|
+
maxLeverage: 1,
|
|
25
|
+
maxMarginUsedPct: 50,
|
|
26
|
+
maxOpenOrders: 25,
|
|
27
|
+
maxOrdersPerMinute: 25,
|
|
28
|
+
maxSlippageBps: 50,
|
|
29
|
+
allowMarketOrders: false,
|
|
30
|
+
allowAccountWideCancel: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
interface GridLevel {
|
|
18
35
|
price: number;
|
|
19
36
|
side: 'buy' | 'sell';
|
|
@@ -58,7 +75,7 @@ export default function grid(api: AutomationAPI) {
|
|
|
58
75
|
|
|
59
76
|
const level: GridLevel = { price, side, size: SIZE };
|
|
60
77
|
|
|
61
|
-
const response = await api.client.limitOrder(COIN, side === 'buy', SIZE, price, 'Gtc', false);
|
|
78
|
+
const response = await api.client.limitOrder(COIN, side === 'buy', SIZE, price, 'Gtc', false, 1);
|
|
62
79
|
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
63
80
|
const status = response.response.data.statuses[0];
|
|
64
81
|
if (status?.resting) {
|
|
@@ -105,7 +122,7 @@ export default function grid(api: AutomationAPI) {
|
|
|
105
122
|
|
|
106
123
|
if (oppositePrice < lower || oppositePrice > upper) continue;
|
|
107
124
|
|
|
108
|
-
const response = await api.client.limitOrder(COIN, oppositeSide === 'buy', SIZE, oppositePrice, 'Gtc', false);
|
|
125
|
+
const response = await api.client.limitOrder(COIN, oppositeSide === 'buy', SIZE, oppositePrice, 'Gtc', false, 1);
|
|
109
126
|
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
110
127
|
const status = response.response.data.statuses[0];
|
|
111
128
|
if (status?.resting) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Maker-Only Market Making — ALO orders that guarantee maker rebates
|
|
2
2
|
|
|
3
|
-
import type { AutomationAPI, AutomationConfig } from '../types.js';
|
|
3
|
+
import type { AutomationAPI, AutomationConfig, AutomationGuardrailContext, AutomationGuardrails } from '../types.js';
|
|
4
4
|
|
|
5
5
|
export const config: AutomationConfig = {
|
|
6
6
|
description: 'Maker-only market making — ALO orders for guaranteed maker rebates',
|
|
@@ -13,6 +13,23 @@ export const config: AutomationConfig = {
|
|
|
13
13
|
},
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export function guardrails({ config: values }: AutomationGuardrailContext): AutomationGuardrails {
|
|
17
|
+
return {
|
|
18
|
+
mode: 'trading',
|
|
19
|
+
allowedMarkets: [String(values.coin ?? 'HYPE')],
|
|
20
|
+
maxOrderUsd: 10_000,
|
|
21
|
+
maxPositionUsd: 25_000,
|
|
22
|
+
maxTotalExposureUsd: 25_000,
|
|
23
|
+
maxLeverage: 1,
|
|
24
|
+
maxMarginUsedPct: 50,
|
|
25
|
+
maxOpenOrders: 10,
|
|
26
|
+
maxOrdersPerMinute: 60,
|
|
27
|
+
maxSlippageBps: 25,
|
|
28
|
+
allowMarketOrders: false,
|
|
29
|
+
allowAccountWideCancel: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
export default function mmMaker(api: AutomationAPI) {
|
|
17
34
|
const COIN = api.state.get<string>('coin', 'HYPE')!;
|
|
18
35
|
const SIZE = api.state.get<number>('size', 0.1)!;
|
|
@@ -95,7 +112,7 @@ export default function mmMaker(api: AutomationAPI) {
|
|
|
95
112
|
|
|
96
113
|
// Place ALO bid
|
|
97
114
|
if (shouldBid && !bidOid && safeBid < book.bestAsk) {
|
|
98
|
-
const resp = await api.client.limitOrder(COIN, true, SIZE, safeBid, 'Alo', false);
|
|
115
|
+
const resp = await api.client.limitOrder(COIN, true, SIZE, safeBid, 'Alo', false, 1);
|
|
99
116
|
if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
|
|
100
117
|
const s = resp.response.data.statuses[0];
|
|
101
118
|
if (s?.resting) { bidOid = s.resting.oid; bidPrice = safeBid; }
|
|
@@ -105,7 +122,7 @@ export default function mmMaker(api: AutomationAPI) {
|
|
|
105
122
|
|
|
106
123
|
// Place ALO ask
|
|
107
124
|
if (shouldAsk && !askOid && safeAsk > book.bestBid) {
|
|
108
|
-
const resp = await api.client.limitOrder(COIN, false, SIZE, safeAsk, 'Alo', false);
|
|
125
|
+
const resp = await api.client.limitOrder(COIN, false, SIZE, safeAsk, 'Alo', false, 1);
|
|
109
126
|
if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
|
|
110
127
|
const s = resp.response.data.statuses[0];
|
|
111
128
|
if (s?.resting) { askOid = s.resting.oid; askPrice = safeAsk; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Market Making (Spread) — Quote bid/ask around mid with inventory skewing
|
|
2
2
|
|
|
3
|
-
import type { AutomationAPI, AutomationConfig } from '../types.js';
|
|
3
|
+
import type { AutomationAPI, AutomationConfig, AutomationGuardrailContext, AutomationGuardrails } from '../types.js';
|
|
4
4
|
|
|
5
5
|
export const config: AutomationConfig = {
|
|
6
6
|
description: 'Market making — quote bid/ask around mid price with inventory skewing',
|
|
@@ -13,6 +13,23 @@ export const config: AutomationConfig = {
|
|
|
13
13
|
},
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export function guardrails({ config: values }: AutomationGuardrailContext): AutomationGuardrails {
|
|
17
|
+
return {
|
|
18
|
+
mode: 'trading',
|
|
19
|
+
allowedMarkets: [String(values.coin ?? 'HYPE')],
|
|
20
|
+
maxOrderUsd: 10_000,
|
|
21
|
+
maxPositionUsd: 25_000,
|
|
22
|
+
maxTotalExposureUsd: 25_000,
|
|
23
|
+
maxLeverage: 1,
|
|
24
|
+
maxMarginUsedPct: 50,
|
|
25
|
+
maxOpenOrders: 10,
|
|
26
|
+
maxOrdersPerMinute: 60,
|
|
27
|
+
maxSlippageBps: 25,
|
|
28
|
+
allowMarketOrders: false,
|
|
29
|
+
allowAccountWideCancel: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
export default function mmSpread(api: AutomationAPI) {
|
|
17
34
|
const COIN = api.state.get<string>('coin', 'HYPE')!;
|
|
18
35
|
const SIZE = api.state.get<number>('size', 0.1)!;
|
|
@@ -91,14 +108,14 @@ export default function mmSpread(api: AutomationAPI) {
|
|
|
91
108
|
|
|
92
109
|
// Place new quotes
|
|
93
110
|
if (shouldBid && !bidOid) {
|
|
94
|
-
const resp = await api.client.limitOrder(COIN, true, SIZE, targetBid, 'Gtc', false);
|
|
111
|
+
const resp = await api.client.limitOrder(COIN, true, SIZE, targetBid, 'Gtc', false, 1);
|
|
95
112
|
if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
|
|
96
113
|
const s = resp.response.data.statuses[0];
|
|
97
114
|
if (s?.resting) { bidOid = s.resting.oid; bidPrice = targetBid; }
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
if (shouldAsk && !askOid) {
|
|
101
|
-
const resp = await api.client.limitOrder(COIN, false, SIZE, targetAsk, 'Gtc', false);
|
|
118
|
+
const resp = await api.client.limitOrder(COIN, false, SIZE, targetAsk, 'Gtc', false, 1);
|
|
102
119
|
if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
|
|
103
120
|
const s = resp.response.data.statuses[0];
|
|
104
121
|
if (s?.resting) { askOid = s.resting.oid; askPrice = targetAsk; }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Price Alert — Real-time price monitoring via WebSocket
|
|
2
2
|
// Showcases WebSocket-driven price_change and order_update events
|
|
3
3
|
|
|
4
|
-
import type { AutomationAPI, AutomationConfig } from '../types.js';
|
|
4
|
+
import type { AutomationAPI, AutomationConfig, AutomationGuardrails } from '../types.js';
|
|
5
5
|
|
|
6
6
|
export const config: AutomationConfig = {
|
|
7
7
|
description: 'Real-time price alerts via WebSocket — log price moves and order updates',
|
|
@@ -13,6 +13,8 @@ export const config: AutomationConfig = {
|
|
|
13
13
|
},
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export const guardrails: AutomationGuardrails = { mode: 'read-only' };
|
|
17
|
+
|
|
16
18
|
export default function priceAlert(api: AutomationAPI) {
|
|
17
19
|
const COIN = api.state.get<string>('coin', 'BTC')!;
|
|
18
20
|
const THRESHOLD = api.state.get<number>('threshold', 0.1)!;
|
|
@@ -83,7 +85,7 @@ export default function priceAlert(api: AutomationAPI) {
|
|
|
83
85
|
);
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
// Periodic summary via
|
|
88
|
+
// Periodic summary via the independent runtime scheduler
|
|
87
89
|
api.on('tick', ({ pollCount }) => {
|
|
88
90
|
if (pollCount % 10 === 0 && alertCount > 0) {
|
|
89
91
|
api.log.info(`Summary: ${alertCount} alerts fired, last price: $${lastAlertPrice.toFixed(2)}`);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import type { HyperliquidClient } from '../core/client.js';
|
|
4
|
+
import {
|
|
5
|
+
CLIENT_WRITE_METHODS,
|
|
6
|
+
GuardrailViolation,
|
|
7
|
+
createGuardrailedClient,
|
|
8
|
+
validateAutomationGuardrails,
|
|
9
|
+
} from './guardrails.js';
|
|
10
|
+
import { listExamples, loadAutomation } from './loader.js';
|
|
11
|
+
import type { AutomationLogger, TradingAutomationGuardrails } from './types.js';
|
|
12
|
+
|
|
13
|
+
const logger: AutomationLogger = {
|
|
14
|
+
info() {},
|
|
15
|
+
warn() {},
|
|
16
|
+
error() {},
|
|
17
|
+
debug() {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function tradingPolicy(overrides: Partial<TradingAutomationGuardrails> = {}): TradingAutomationGuardrails {
|
|
21
|
+
return {
|
|
22
|
+
mode: 'trading',
|
|
23
|
+
allowedMarkets: ['ETH'],
|
|
24
|
+
maxOrderUsd: 1_000,
|
|
25
|
+
maxPositionUsd: 2_000,
|
|
26
|
+
maxTotalExposureUsd: 5_000,
|
|
27
|
+
maxLeverage: 2,
|
|
28
|
+
maxMarginUsedPct: 50,
|
|
29
|
+
maxOpenOrders: 10,
|
|
30
|
+
maxOrdersPerMinute: 10,
|
|
31
|
+
maxSlippageBps: 40,
|
|
32
|
+
allowMarketOrders: true,
|
|
33
|
+
allowAccountWideCancel: false,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mockClient(options: {
|
|
39
|
+
positions?: Array<{ coin: string; size: number; price: number; leverage?: number }>;
|
|
40
|
+
spotBalances?: Array<{ coin: string; total: string }>;
|
|
41
|
+
spotData?: {
|
|
42
|
+
meta: {
|
|
43
|
+
tokens: Array<{ index: number; name: string }>;
|
|
44
|
+
universe: Array<{ name: string; tokens: [number, number] }>;
|
|
45
|
+
};
|
|
46
|
+
assetCtxs: Array<{ coin?: string; midPx: string; markPx: string }>;
|
|
47
|
+
};
|
|
48
|
+
} = {}) {
|
|
49
|
+
const calls: Array<{ method: string; args: unknown[] }> = [];
|
|
50
|
+
const client = {
|
|
51
|
+
address: '0x0000000000000000000000000000000000000001',
|
|
52
|
+
walletAddress: '0x0000000000000000000000000000000000000002',
|
|
53
|
+
isApiWallet: true,
|
|
54
|
+
async getUserStateAll() {
|
|
55
|
+
return {
|
|
56
|
+
assetPositions: (options.positions ?? []).map((position) => ({
|
|
57
|
+
position: {
|
|
58
|
+
coin: position.coin,
|
|
59
|
+
szi: String(position.size),
|
|
60
|
+
positionValue: String(Math.abs(position.size * position.price)),
|
|
61
|
+
leverage: { type: 'cross', value: position.leverage ?? 1 },
|
|
62
|
+
},
|
|
63
|
+
})),
|
|
64
|
+
marginSummary: { accountValue: '10000', totalMarginUsed: '0' },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
async getAllMids() { return { ETH: '2000', BTC: '50000' }; },
|
|
68
|
+
async getOpenOrders() { return []; },
|
|
69
|
+
async getSpotBalances() { return { balances: options.spotBalances ?? [] }; },
|
|
70
|
+
async getSpotMetaAndAssetCtxs() {
|
|
71
|
+
return options.spotData ?? { meta: { tokens: [], universe: [] }, assetCtxs: [] };
|
|
72
|
+
},
|
|
73
|
+
resolveOutcomeRef() {
|
|
74
|
+
return { outcome: 1, side: 0, encoding: 10, coin: '#10', tokenName: '+10', assetId: 100000010 };
|
|
75
|
+
},
|
|
76
|
+
async marketOrder(...args: unknown[]) {
|
|
77
|
+
calls.push({ method: 'marketOrder', args });
|
|
78
|
+
return { status: 'ok' };
|
|
79
|
+
},
|
|
80
|
+
async limitOrder(...args: unknown[]) {
|
|
81
|
+
calls.push({ method: 'limitOrder', args });
|
|
82
|
+
return { status: 'ok' };
|
|
83
|
+
},
|
|
84
|
+
async bulkOrder(...args: unknown[]) {
|
|
85
|
+
calls.push({ method: 'bulkOrder', args });
|
|
86
|
+
return { status: 'ok' };
|
|
87
|
+
},
|
|
88
|
+
async spotLimitOrder(...args: unknown[]) {
|
|
89
|
+
calls.push({ method: 'spotLimitOrder', args });
|
|
90
|
+
return { status: 'ok' };
|
|
91
|
+
},
|
|
92
|
+
} as unknown as HyperliquidClient;
|
|
93
|
+
return { client, calls };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('guardrail schema rejects missing and internally inconsistent limits', () => {
|
|
97
|
+
assert.throws(
|
|
98
|
+
() => validateAutomationGuardrails({ mode: 'trading' }),
|
|
99
|
+
/allowedMarkets/,
|
|
100
|
+
);
|
|
101
|
+
assert.throws(
|
|
102
|
+
() => validateAutomationGuardrails(tradingPolicy({ maxOrderUsd: 3_000 })),
|
|
103
|
+
/maxOrderUsd.*maxPositionUsd/,
|
|
104
|
+
);
|
|
105
|
+
assert.deepEqual(validateAutomationGuardrails({ mode: 'read-only' }), { mode: 'read-only' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('all client write families are included in the enforcement boundary', () => {
|
|
109
|
+
for (const method of [
|
|
110
|
+
'bulkOrder', 'bulkCancel', 'scheduleCancel',
|
|
111
|
+
'outcomeOrder', 'outcomeMarketOrder', 'outcomeLimitOrder',
|
|
112
|
+
]) {
|
|
113
|
+
assert.equal(CLIENT_WRITE_METHODS.has(method), true, `${method} must be guarded`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('every bundled example exports a valid guardrail policy', async () => {
|
|
118
|
+
for (const example of listExamples()) {
|
|
119
|
+
const loaded = await loadAutomation(example.path, { config: {} });
|
|
120
|
+
assert.ok(loaded.guardrails.mode === 'read-only' || loaded.guardrails.mode === 'trading');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('read-only policy blocks every write before it reaches the client', async () => {
|
|
125
|
+
const { client, calls } = mockClient();
|
|
126
|
+
const guarded = createGuardrailedClient(client, {
|
|
127
|
+
policy: { mode: 'read-only' },
|
|
128
|
+
rawClient: client,
|
|
129
|
+
log: logger,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await assert.rejects(
|
|
133
|
+
guarded.bulkOrder([{ coin: 'ETH', isBuy: true, size: 0.1, price: 2000 }]),
|
|
134
|
+
(error: unknown) => error instanceof GuardrailViolation && error.code === 'read-only',
|
|
135
|
+
);
|
|
136
|
+
assert.equal(calls.length, 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('trading policy blocks disallowed markets, missing leverage, and oversized orders', async () => {
|
|
140
|
+
const { client, calls } = mockClient();
|
|
141
|
+
const guarded = createGuardrailedClient(client, {
|
|
142
|
+
policy: tradingPolicy(),
|
|
143
|
+
rawClient: client,
|
|
144
|
+
log: logger,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await assert.rejects(
|
|
148
|
+
guarded.limitOrder('BTC', true, 0.01, 50_000, 'Gtc', false, 1),
|
|
149
|
+
(error: unknown) => error instanceof GuardrailViolation && error.code === 'market-not-allowed',
|
|
150
|
+
);
|
|
151
|
+
await assert.rejects(
|
|
152
|
+
guarded.limitOrder('ETH', true, 0.1, 2_000),
|
|
153
|
+
(error: unknown) => error instanceof GuardrailViolation && error.code === 'leverage-required',
|
|
154
|
+
);
|
|
155
|
+
await assert.rejects(
|
|
156
|
+
guarded.limitOrder('ETH', true, 0.6, 2_000, 'Gtc', false, 1),
|
|
157
|
+
(error: unknown) => error instanceof GuardrailViolation && error.code === 'order-notional',
|
|
158
|
+
);
|
|
159
|
+
assert.equal(calls.length, 0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('valid market orders execute with runtime-capped slippage', async () => {
|
|
163
|
+
const { client, calls } = mockClient();
|
|
164
|
+
const guarded = createGuardrailedClient(client, {
|
|
165
|
+
policy: tradingPolicy(),
|
|
166
|
+
rawClient: client,
|
|
167
|
+
log: logger,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await guarded.marketOrder('ETH', true, 0.1, undefined, 1);
|
|
171
|
+
assert.equal(calls.length, 1);
|
|
172
|
+
assert.deepEqual(calls[0], {
|
|
173
|
+
method: 'marketOrder',
|
|
174
|
+
args: ['ETH', true, 0.1, 40, 1],
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('account-wide exposure is checked while genuine reductions remain available above caps', async () => {
|
|
179
|
+
const exposed = mockClient({ positions: [{ coin: 'BTC', size: 0.1, price: 50_000 }] });
|
|
180
|
+
const exposureGuarded = createGuardrailedClient(exposed.client, {
|
|
181
|
+
policy: tradingPolicy({ maxTotalExposureUsd: 5_100 }),
|
|
182
|
+
rawClient: exposed.client,
|
|
183
|
+
log: logger,
|
|
184
|
+
});
|
|
185
|
+
await assert.rejects(
|
|
186
|
+
exposureGuarded.limitOrder('ETH', true, 0.1, 2_000, 'Gtc', false, 1),
|
|
187
|
+
(error: unknown) => error instanceof GuardrailViolation && error.code === 'total-exposure',
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const reducing = mockClient({ positions: [{ coin: 'ETH', size: 1, price: 2_000 }] });
|
|
191
|
+
const reductionGuarded = createGuardrailedClient(reducing.client, {
|
|
192
|
+
policy: tradingPolicy({ maxPositionUsd: 1_000, maxTotalExposureUsd: 1_000 }),
|
|
193
|
+
rawClient: reducing.client,
|
|
194
|
+
log: logger,
|
|
195
|
+
});
|
|
196
|
+
await reductionGuarded.marketOrder('ETH', false, 0.1);
|
|
197
|
+
assert.equal(reducing.calls.length, 1);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('spot exposure joins market contexts by pair identifier instead of array position', async () => {
|
|
201
|
+
const { client, calls } = mockClient({
|
|
202
|
+
spotBalances: [{ coin: 'HYPE', total: '3' }],
|
|
203
|
+
spotData: {
|
|
204
|
+
meta: {
|
|
205
|
+
tokens: [{ index: 0, name: 'USDC' }, { index: 1, name: 'HYPE' }],
|
|
206
|
+
universe: [{ name: '@107', tokens: [1, 0] }],
|
|
207
|
+
},
|
|
208
|
+
assetCtxs: [
|
|
209
|
+
{ coin: '@999', midPx: '99999', markPx: '99999' },
|
|
210
|
+
{ coin: '@107', midPx: '62.5', markPx: '62.5' },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const guarded = createGuardrailedClient(client, {
|
|
215
|
+
policy: tradingPolicy({
|
|
216
|
+
allowedMarkets: ['spot:HYPE'],
|
|
217
|
+
maxOrderUsd: 100,
|
|
218
|
+
maxPositionUsd: 500,
|
|
219
|
+
maxTotalExposureUsd: 500,
|
|
220
|
+
}),
|
|
221
|
+
rawClient: client,
|
|
222
|
+
log: logger,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await guarded.spotLimitOrder('HYPE', true, 0.1, 62.5, 'Gtc');
|
|
226
|
+
assert.equal(calls.length, 1);
|
|
227
|
+
});
|