openbroker 1.9.1 → 1.9.3

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 (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +11 -0
  3. package/SKILL.md +58 -1
  4. package/bin/cli.ts +3 -0
  5. package/dist/auto/cli.js +3 -0
  6. package/dist/auto/examples/dca.d.ts +2 -1
  7. package/dist/auto/examples/dca.d.ts.map +1 -1
  8. package/dist/auto/examples/dca.js +19 -1
  9. package/dist/auto/examples/funding-arb.d.ts +2 -1
  10. package/dist/auto/examples/funding-arb.d.ts.map +1 -1
  11. package/dist/auto/examples/funding-arb.js +19 -2
  12. package/dist/auto/examples/grid.d.ts +2 -1
  13. package/dist/auto/examples/grid.d.ts.map +1 -1
  14. package/dist/auto/examples/grid.js +18 -2
  15. package/dist/auto/examples/mm-maker.d.ts +2 -1
  16. package/dist/auto/examples/mm-maker.d.ts.map +1 -1
  17. package/dist/auto/examples/mm-maker.js +18 -2
  18. package/dist/auto/examples/mm-spread.d.ts +2 -1
  19. package/dist/auto/examples/mm-spread.d.ts.map +1 -1
  20. package/dist/auto/examples/mm-spread.js +18 -2
  21. package/dist/auto/examples/price-alert.d.ts +2 -1
  22. package/dist/auto/examples/price-alert.d.ts.map +1 -1
  23. package/dist/auto/examples/price-alert.js +1 -0
  24. package/dist/auto/guardrails.d.ts +19 -0
  25. package/dist/auto/guardrails.d.ts.map +1 -0
  26. package/dist/auto/guardrails.js +575 -0
  27. package/dist/auto/guardrails.test.d.ts +2 -0
  28. package/dist/auto/guardrails.test.d.ts.map +1 -0
  29. package/dist/auto/guardrails.test.js +173 -0
  30. package/dist/auto/loader.d.ts +3 -3
  31. package/dist/auto/loader.d.ts.map +1 -1
  32. package/dist/auto/loader.js +25 -3
  33. package/dist/auto/runtime.d.ts.map +1 -1
  34. package/dist/auto/runtime.js +38 -20
  35. package/dist/auto/types.d.ts +43 -0
  36. package/dist/auto/types.d.ts.map +1 -1
  37. package/dist/lib.d.ts +2 -0
  38. package/dist/lib.d.ts.map +1 -1
  39. package/dist/lib.js +1 -0
  40. package/dist/setup/install.d.ts +3 -0
  41. package/dist/setup/install.d.ts.map +1 -0
  42. package/dist/setup/install.js +113 -0
  43. package/package.json +4 -3
  44. package/scripts/auto/cli.ts +3 -0
  45. package/scripts/auto/examples/dca.ts +21 -2
  46. package/scripts/auto/examples/funding-arb.ts +21 -3
  47. package/scripts/auto/examples/grid.ts +20 -3
  48. package/scripts/auto/examples/mm-maker.ts +20 -3
  49. package/scripts/auto/examples/mm-spread.ts +20 -3
  50. package/scripts/auto/examples/price-alert.ts +3 -1
  51. package/scripts/auto/guardrails.test.ts +227 -0
  52. package/scripts/auto/guardrails.ts +700 -0
  53. package/scripts/auto/loader.ts +41 -4
  54. package/scripts/auto/runtime.ts +38 -22
  55. package/scripts/auto/types.ts +54 -0
  56. package/scripts/lib.ts +10 -0
  57. package/scripts/setup/install.ts +146 -0
@@ -1,6 +1,6 @@
1
1
  // DCA (Dollar Cost Averaging) — Buy fixed USD amounts at regular intervals
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: 'Dollar cost averaging — buy fixed USD amounts at regular intervals',
@@ -12,6 +12,25 @@ export const config: AutomationConfig = {
12
12
  },
13
13
  };
14
14
 
15
+ export function guardrails({ config: values }: AutomationGuardrailContext): AutomationGuardrails {
16
+ const amount = Number(values.amount ?? 100);
17
+ const count = Number(values.count ?? 24);
18
+ return {
19
+ mode: 'trading',
20
+ allowedMarkets: [String(values.coin ?? 'HYPE')],
21
+ maxOrderUsd: amount * 1.1,
22
+ maxPositionUsd: amount * count * 1.1,
23
+ maxTotalExposureUsd: amount * count * 1.1,
24
+ maxLeverage: 1,
25
+ maxMarginUsedPct: 50,
26
+ maxOpenOrders: 5,
27
+ maxOrdersPerMinute: 5,
28
+ maxSlippageBps: 50,
29
+ allowMarketOrders: true,
30
+ allowAccountWideCancel: false,
31
+ };
32
+ }
33
+
15
34
  export default function dca(api: AutomationAPI) {
16
35
  const COIN = api.state.get<string>('coin', 'HYPE')!;
17
36
  const AMOUNT_USD = api.state.get<number>('amount', 100)!;
@@ -43,7 +62,7 @@ export default function dca(api: AutomationAPI) {
43
62
  const size = AMOUNT_USD / price;
44
63
  api.log.info(`[${purchased + 1}/${MAX_PURCHASES}] Buying ~$${AMOUNT_USD} of ${COIN} @ $${price.toFixed(2)}`);
45
64
 
46
- const response = await api.client.marketOrder(COIN, true, size);
65
+ const response = await api.client.marketOrder(COIN, true, size, undefined, 1);
47
66
  if (response.status === 'ok' && response.response && typeof response.response === 'object') {
48
67
  const status = response.response.data.statuses[0];
49
68
  if (status?.filled) {
@@ -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)!;
@@ -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
+ });