openbroker 1.0.67 → 1.0.68

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/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
4
4
  license: MIT
5
5
  compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
6
6
  homepage: https://www.npmjs.com/package/openbroker
7
- metadata: {"author": "monemetrics", "version": "1.0.67", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
7
+ metadata: {"author": "monemetrics", "version": "1.0.68", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
8
8
  allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -234,7 +234,6 @@ All trading commands support HIP-3 assets using `dex:COIN` syntax:
234
234
  openbroker buy --coin xyz:CL --size 1 # Buy crude oil on xyz dex
235
235
  openbroker sell --coin xyz:BRENTOIL --size 1 # Sell brent oil
236
236
  openbroker limit --coin xyz:GOLD --side buy --size 0.1 --price 2500
237
- openbroker funding-arb --coin xyz:CL --size 5000 # Funding arb on HIP-3
238
237
  ```
239
238
 
240
239
  ### Market Orders (Quick)
@@ -325,53 +324,6 @@ openbroker chase --coin ETH --side buy --size 0.5 --timeout 300
325
324
  openbroker chase --coin SOL --side buy --size 10 --offset 2 --timeout 60
326
325
  ```
327
326
 
328
- ## Trading Strategies
329
-
330
- ### Funding Arbitrage
331
- ```bash
332
- # Collect funding on ETH if rate > 25% annualized
333
- openbroker funding-arb --coin ETH --size 5000 --min-funding 25
334
-
335
- # Run for 24 hours, check every 30 minutes
336
- openbroker funding-arb --coin BTC --size 10000 --duration 24 --check 30 --dry
337
- ```
338
-
339
- ### Grid Trading
340
- ```bash
341
- # ETH grid from $3000-$4000 with 10 levels, 0.1 ETH per level
342
- openbroker grid --coin ETH --lower 3000 --upper 4000 --grids 10 --size 0.1
343
-
344
- # Accumulation grid (buys only)
345
- openbroker grid --coin BTC --lower 90000 --upper 100000 --grids 5 --size 0.01 --mode long
346
- ```
347
-
348
- ### DCA (Dollar Cost Averaging)
349
- ```bash
350
- # Buy $100 of ETH every hour for 24 hours
351
- openbroker dca --coin ETH --amount 100 --interval 1h --count 24
352
-
353
- # Invest $5000 in BTC over 30 days with daily purchases
354
- openbroker dca --coin BTC --total 5000 --interval 1d --count 30
355
- ```
356
-
357
- ### Market Making Spread
358
- ```bash
359
- # Market make ETH with 0.1 size, 10bps spread
360
- openbroker mm-spread --coin ETH --size 0.1 --spread 10
361
-
362
- # Tighter spread with position limit
363
- openbroker mm-spread --coin BTC --size 0.01 --spread 5 --max-position 0.1
364
- ```
365
-
366
- ### Maker-Only MM (ALO orders)
367
- ```bash
368
- # Market make using ALO (post-only) orders - guarantees maker rebates
369
- openbroker mm-maker --coin HYPE --size 1 --offset 1
370
-
371
- # Wider offset for volatile assets
372
- openbroker mm-maker --coin ETH --size 0.1 --offset 2 --max-position 0.5
373
- ```
374
-
375
327
  ## Order Types
376
328
 
377
329
  ### Limit Orders vs Trigger Orders
@@ -495,7 +447,24 @@ Without hooks, the watcher still runs and tracks state (accessible via `ob_watch
495
447
 
496
448
  ## Trading Automations
497
449
 
498
- Automations let you write custom event-driven trading logic as TypeScript scripts. Instead of using the rigid built-in strategies, write exactly the logic you need and OpenBroker handles the polling, event detection, and SDK access.
450
+ Automations let you write custom event-driven trading logic as TypeScript scripts. Write exactly the logic you need and OpenBroker handles the polling, event detection, and SDK access.
451
+
452
+ ### IMPORTANT: Bundled Examples Are References, Not Ready-Made Strategies
453
+
454
+ OpenBroker ships bundled automation examples (`dca`, `grid`, `funding-arb`, `mm-spread`, `mm-maker`) that demonstrate common patterns. **These examples are meant to be read and learned from — not run directly as production strategies.** When the user asks for a strategy (e.g. "run a DCA on HYPE"), you should:
455
+
456
+ 1. Read the relevant bundled example source code to understand the automation API patterns
457
+ 2. Write a **custom automation** tailored to the user's specific requirements (coin, size, thresholds, risk parameters, TP/SL logic, etc.)
458
+ 3. Save it to `~/.openbroker/automations/` and run it with `openbroker auto run`
459
+
460
+ Do NOT simply run `openbroker auto run --example dca --set coin=HYPE`. The examples use generic defaults and lack the nuanced risk management, position sizing, and TP/SL logic that a real strategy needs. Always write a purpose-built automation.
461
+
462
+ To view bundled examples and their config schemas:
463
+ ```bash
464
+ openbroker auto examples # List examples with config fields
465
+ ```
466
+
467
+ Available examples: `dca`, `grid`, `funding-arb`, `mm-spread`, `mm-maker`
499
468
 
500
469
  ### How Automations Work
501
470
 
@@ -897,14 +866,17 @@ export default function(api) {
897
866
  openbroker auto run my-strategy --dry # Test without trading
898
867
  openbroker auto run ./funding-scalp.ts # Run from path
899
868
  openbroker auto run my-strategy --poll 5000 # Poll every 5s
869
+ openbroker auto run --example dca --set coin=HYPE --set amount=50 --dry # Run bundled example
870
+ openbroker auto examples # List bundled examples with config
900
871
  openbroker auto list # Show available scripts
901
872
  openbroker auto status # Show running automations
902
873
  ```
903
874
 
904
875
  **Plugin tools (for OpenClaw agents):**
905
876
  - `ob_auto_run` — `{ "script": "funding-scalp", "dry": true }` — start an automation
877
+ - `ob_auto_run` — `{ "example": "dca", "config": { "coin": "HYPE", "amount": 50 }, "dry": true }` — run a bundled example
906
878
  - `ob_auto_stop` — `{ "id": "funding-scalp" }` — stop a running automation
907
- - `ob_auto_list` — `{}` — list available and running automations
879
+ - `ob_auto_list` — `{}` — list available automations, bundled examples with config schemas, and running automations
908
880
 
909
881
  **Options:**
910
882
  | Flag | Description | Default |
@@ -913,6 +885,8 @@ openbroker auto status # Show running automations
913
885
  | `--verbose` | Show debug output | false |
914
886
  | `--id <name>` | Custom automation ID | filename |
915
887
  | `--poll <ms>` | Poll interval in milliseconds | 10000 |
888
+ | `--example <name>` | Run a bundled example automation | - |
889
+ | `--set key=value` | Set config values (repeatable) | - |
916
890
 
917
891
  **Guidelines for agents writing automations:**
918
892
 
package/bin/cli.ts CHANGED
@@ -46,13 +46,6 @@ const commands: Record<string, { script: string; description: string }> = {
46
46
  'bracket': { script: 'operations/bracket.ts', description: 'Bracket order (entry + TP + SL)' },
47
47
  'chase': { script: 'operations/chase.ts', description: 'Chase order with ALO' },
48
48
 
49
- // Strategies
50
- 'funding-arb': { script: 'strategies/funding-arb.ts', description: 'Funding arbitrage strategy' },
51
- 'grid': { script: 'strategies/grid.ts', description: 'Grid trading strategy' },
52
- 'dca': { script: 'strategies/dca.ts', description: 'DCA strategy' },
53
- 'mm-spread': { script: 'strategies/mm-spread.ts', description: 'Market making (spread)' },
54
- 'mm-maker': { script: 'strategies/mm-maker.ts', description: 'Market making (ALO)' },
55
-
56
49
  // Automations
57
50
  'auto': { script: 'auto/cli.ts', description: 'Run/manage trading automations' },
58
51
  };
@@ -99,15 +92,10 @@ Advanced Execution:
99
92
  bracket Entry with TP and SL
100
93
  chase Chase price with ALO orders
101
94
 
102
- Strategies:
103
- funding-arb Funding rate arbitrage
104
- grid Grid trading
105
- dca Dollar cost averaging
106
- mm-spread Market making (spread-based)
107
- mm-maker Market making (ALO orders)
108
-
109
95
  Automations:
110
96
  auto run <script> Run a custom trading automation
97
+ auto run --example <name> [--set key=value] Run a bundled example
98
+ auto examples List bundled examples (dca, grid, funding-arb, mm-spread, mm-maker)
111
99
  auto list List available automations
112
100
  auto status Show running automations
113
101
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.67",
4
+ "version": "1.0.68",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,11 +39,6 @@
39
39
  "bracket": "tsx scripts/operations/bracket.ts",
40
40
  "chase": "tsx scripts/operations/chase.ts",
41
41
  "funding-scan": "tsx scripts/info/funding-scan.ts",
42
- "funding-arb": "tsx scripts/strategies/funding-arb.ts",
43
- "grid": "tsx scripts/strategies/grid.ts",
44
- "dca": "tsx scripts/strategies/dca.ts",
45
- "mm-spread": "tsx scripts/strategies/mm-spread.ts",
46
- "mm-maker": "tsx scripts/strategies/mm-maker.ts",
47
42
  "prepublishOnly": "npm run test:cli",
48
43
  "test:cli": "node --import tsx bin/cli.ts --help"
49
44
  },
@@ -62,11 +57,11 @@
62
57
  },
63
58
  "repository": {
64
59
  "type": "git",
65
- "url": "git+https://github.com/aurracloud/openbroker.git"
60
+ "url": "git+https://github.com/aurracloud/open-broker.git"
66
61
  },
67
- "homepage": "https://github.com/aurracloud/openbroker#readme",
62
+ "homepage": "https://github.com/aurracloud/open-broker#readme",
68
63
  "bugs": {
69
- "url": "https://github.com/aurracloud/openbroker/issues"
64
+ "url": "https://github.com/aurracloud/open-broker/issues"
70
65
  },
71
66
  "author": "aurracloud",
72
67
  "keywords": [
@@ -1,7 +1,7 @@
1
1
  // CLI entry point for `openbroker auto` commands
2
2
 
3
3
  import { parseArgs } from '../core/utils.js';
4
- import { resolveScriptPath, listAutomations, ensureAutomationsDir } from './loader.js';
4
+ import { resolveScriptPath, resolveExamplePath, listAutomations, listExamples, loadExampleConfigs, ensureAutomationsDir } from './loader.js';
5
5
  import { startAutomation, getRunningAutomations, getRegisteredAutomations } from './runtime.js';
6
6
  import { unregisterAutomation, cleanRegistry } from './registry.js';
7
7
 
@@ -11,12 +11,16 @@ OpenBroker Automations — event-driven trading scripts
11
11
 
12
12
  Usage:
13
13
  openbroker auto run <script> [options] Run an automation script
14
+ openbroker auto run --example <name> Run a bundled example automation
15
+ openbroker auto examples List bundled example automations
14
16
  openbroker auto stop <id> Unregister an automation (won't restart)
15
17
  openbroker auto list List available automations
16
18
  openbroker auto status Show running automations
17
19
  openbroker auto clean Remove stale entries from registry
18
20
 
19
21
  Options (for run):
22
+ --example <name> Run a bundled example (dca, grid, funding-arb, mm-spread, mm-maker)
23
+ --set key=value Set config values (repeatable, auto-parses numbers/booleans)
20
24
  --dry Intercept write methods (no real trades)
21
25
  --verbose Show debug output
22
26
  --id <name> Custom automation ID (default: filename)
@@ -25,6 +29,7 @@ Options (for run):
25
29
  Scripts are loaded from:
26
30
  1. Absolute or relative path
27
31
  2. ~/.openbroker/automations/<name>.ts
32
+ 3. Bundled examples (via --example)
28
33
 
29
34
  Writing an automation:
30
35
  export default function(api) {
@@ -37,21 +42,66 @@ Events: tick, price_change, funding_update, position_opened,
37
42
  position_closed, position_changed, pnl_threshold, margin_warning
38
43
 
39
44
  Examples:
40
- openbroker auto run my-strategy --dry # Test without trading
41
- openbroker auto run ./funding-scalp.ts # Run from path
42
- openbroker auto list # Show available scripts
45
+ openbroker auto run --example dca --set coin=BTC --set amount=50 --dry
46
+ openbroker auto run --example grid --set coin=ETH --set lower=3000 --set upper=4000
47
+ openbroker auto run my-strategy --dry
48
+ openbroker auto examples
43
49
  `);
44
50
  }
45
51
 
46
- async function runCommand(args: Record<string, string | boolean>, positional: string[]) {
52
+ /** Parse --set key=value flags from raw args, return parsed config object */
53
+ function parseSetFlags(rawArgs: string[]): Record<string, unknown> {
54
+ const config: Record<string, unknown> = {};
55
+
56
+ for (let i = 0; i < rawArgs.length; i++) {
57
+ if (rawArgs[i] === '--set' && i + 1 < rawArgs.length) {
58
+ const pair = rawArgs[i + 1];
59
+ const eqIdx = pair.indexOf('=');
60
+ if (eqIdx === -1) {
61
+ console.error(`Error: --set requires key=value format, got: ${pair}`);
62
+ process.exit(1);
63
+ }
64
+ const key = pair.slice(0, eqIdx);
65
+ const raw = pair.slice(eqIdx + 1);
66
+
67
+ // Auto-parse numbers and booleans
68
+ if (raw === 'true') config[key] = true;
69
+ else if (raw === 'false') config[key] = false;
70
+ else if (raw !== '' && !isNaN(Number(raw))) config[key] = Number(raw);
71
+ else config[key] = raw;
72
+
73
+ i++; // skip the value
74
+ }
75
+ }
76
+
77
+ return config;
78
+ }
79
+
80
+ /** Strip --set key=value pairs from raw args so parseArgs doesn't see them */
81
+ function stripSetFlags(rawArgs: string[]): string[] {
82
+ const result: string[] = [];
83
+ for (let i = 0; i < rawArgs.length; i++) {
84
+ if (rawArgs[i] === '--set' && i + 1 < rawArgs.length) {
85
+ i++; // skip --set and its value
86
+ } else {
87
+ result.push(rawArgs[i]);
88
+ }
89
+ }
90
+ return result;
91
+ }
92
+
93
+ async function runCommand(args: Record<string, string | boolean>, positional: string[], initialState: Record<string, unknown>) {
94
+ const exampleName = args.example ? String(args.example) : undefined;
47
95
  const scriptName = positional[0];
48
- if (!scriptName) {
49
- console.error('Error: script name or path required');
96
+
97
+ if (!scriptName && !exampleName) {
98
+ console.error('Error: script name or path required (or use --example <name>)');
50
99
  console.log('Usage: openbroker auto run <script> [--dry] [--verbose]');
100
+ console.log(' openbroker auto run --example <name> [--set key=value] [--dry]');
51
101
  process.exit(1);
52
102
  }
53
103
 
54
- const scriptPath = resolveScriptPath(scriptName);
104
+ const scriptPath = exampleName ? resolveExamplePath(exampleName) : resolveScriptPath(scriptName!);
55
105
  const dryRun = args.dry === true;
56
106
  const verbose = args.verbose === true;
57
107
  const pollIntervalMs = args.poll ? parseInt(String(args.poll), 10) : 10_000;
@@ -68,6 +118,7 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
68
118
  dryRun,
69
119
  verbose,
70
120
  pollIntervalMs,
121
+ initialState: Object.keys(initialState).length > 0 ? initialState : undefined,
71
122
  });
72
123
 
73
124
  // Graceful shutdown on SIGINT/SIGTERM
@@ -84,22 +135,58 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
84
135
  await new Promise(() => {});
85
136
  }
86
137
 
138
+ async function examplesCommand() {
139
+ const configs = await loadExampleConfigs();
140
+ const names = Object.keys(configs);
141
+
142
+ if (names.length === 0) {
143
+ console.log('No bundled examples found.');
144
+ return;
145
+ }
146
+
147
+ console.log('Bundled example automations:\n');
148
+ for (const name of names) {
149
+ const cfg = configs[name];
150
+ console.log(` ${name}`);
151
+ console.log(` ${cfg.description}\n`);
152
+
153
+ console.log(` Config (--set key=value):`);
154
+ for (const [key, field] of Object.entries(cfg.fields)) {
155
+ const def = JSON.stringify(field.default);
156
+ console.log(` ${key.padEnd(14)} ${field.type.padEnd(8)} ${field.description} (default: ${def})`);
157
+ }
158
+ console.log('');
159
+ }
160
+
161
+ console.log(`Run with: openbroker auto run --example <name> [--set key=value] [--dry]`);
162
+ console.log('Copy to ~/.openbroker/automations/ to customize.');
163
+ }
164
+
87
165
  function listCommand() {
88
166
  ensureAutomationsDir();
89
167
  const automations = listAutomations();
168
+ const examples = listExamples();
90
169
 
91
- if (automations.length === 0) {
170
+ if (automations.length === 0 && examples.length === 0) {
92
171
  console.log('No automations found in ~/.openbroker/automations/');
93
172
  console.log('\nCreate a .ts file there with:');
94
173
  console.log(' export default function(api) { ... }');
174
+ console.log('\nOr run a bundled example: openbroker auto examples');
95
175
  return;
96
176
  }
97
177
 
98
- console.log('Available automations:\n');
99
- for (const a of automations) {
100
- console.log(` ${a.name.padEnd(30)} ${a.path}`);
178
+ if (automations.length > 0) {
179
+ console.log('User automations (~/.openbroker/automations/):\n');
180
+ for (const a of automations) {
181
+ console.log(` ${a.name.padEnd(30)} ${a.path}`);
182
+ }
183
+ console.log(`\nRun with: openbroker auto run <name>`);
184
+ }
185
+
186
+ if (examples.length > 0) {
187
+ if (automations.length > 0) console.log('');
188
+ console.log(`${examples.length} bundled examples available — run: openbroker auto examples`);
101
189
  }
102
- console.log(`\nRun with: openbroker auto run <name>`);
103
190
  }
104
191
 
105
192
  function statusCommand() {
@@ -192,19 +279,23 @@ async function main() {
192
279
  const subcommand = rawArgs[0];
193
280
  const restArgs = rawArgs.slice(1);
194
281
 
282
+ // Parse --set flags before stripping them
283
+ const initialState = parseSetFlags(restArgs);
284
+ const cleanedArgs = stripSetFlags(restArgs);
285
+
195
286
  // Extract positional args (non-flag args)
196
287
  const positional: string[] = [];
197
288
  const flagArgs: string[] = [];
198
- for (let i = 0; i < restArgs.length; i++) {
199
- if (restArgs[i].startsWith('--')) {
200
- flagArgs.push(restArgs[i]);
289
+ for (let i = 0; i < cleanedArgs.length; i++) {
290
+ if (cleanedArgs[i].startsWith('--')) {
291
+ flagArgs.push(cleanedArgs[i]);
201
292
  // If next arg doesn't start with --, it's a flag value
202
- if (i + 1 < restArgs.length && !restArgs[i + 1].startsWith('--')) {
203
- flagArgs.push(restArgs[i + 1]);
293
+ if (i + 1 < cleanedArgs.length && !cleanedArgs[i + 1].startsWith('--')) {
294
+ flagArgs.push(cleanedArgs[i + 1]);
204
295
  i++;
205
296
  }
206
297
  } else {
207
- positional.push(restArgs[i]);
298
+ positional.push(cleanedArgs[i]);
208
299
  }
209
300
  }
210
301
 
@@ -212,7 +303,7 @@ async function main() {
212
303
 
213
304
  switch (subcommand) {
214
305
  case 'run':
215
- await runCommand(args, positional);
306
+ await runCommand(args, positional, initialState);
216
307
  break;
217
308
  case 'stop':
218
309
  stopCommand(positional);
@@ -220,6 +311,9 @@ async function main() {
220
311
  case 'list':
221
312
  listCommand();
222
313
  break;
314
+ case 'examples':
315
+ await examplesCommand();
316
+ break;
223
317
  case 'status':
224
318
  statusCommand();
225
319
  break;
@@ -0,0 +1,72 @@
1
+ // DCA (Dollar Cost Averaging) — Buy fixed USD amounts at regular intervals
2
+
3
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
4
+
5
+ export const config: AutomationConfig = {
6
+ description: 'Dollar cost averaging — buy fixed USD amounts at regular intervals',
7
+ fields: {
8
+ coin: { type: 'string', description: 'Asset to accumulate', default: 'HYPE' },
9
+ amount: { type: 'number', description: 'USD per purchase', default: 100 },
10
+ interval: { type: 'number', description: 'Milliseconds between buys (3600000 = 1h)', default: 3_600_000 },
11
+ count: { type: 'number', description: 'Total number of purchases', default: 24 },
12
+ },
13
+ };
14
+
15
+ export default function dca(api: AutomationAPI) {
16
+ const COIN = api.state.get<string>('coin', 'HYPE')!;
17
+ const AMOUNT_USD = api.state.get<number>('amount', 100)!;
18
+ const INTERVAL_MS = api.state.get<number>('interval', 3_600_000)!;
19
+ const MAX_PURCHASES = api.state.get<number>('count', 24)!;
20
+
21
+ let purchased = api.state.get<number>('purchased', 0)!;
22
+ let totalSpent = api.state.get<number>('totalSpent', 0)!;
23
+ let totalAcquired = api.state.get<number>('totalAcquired', 0)!;
24
+
25
+ api.onStart(() => {
26
+ api.log.info(`DCA: ${MAX_PURCHASES} buys of $${AMOUNT_USD} ${COIN} every ${INTERVAL_MS / 60000}m`);
27
+ api.log.info(`Progress: ${purchased}/${MAX_PURCHASES} completed`);
28
+ });
29
+
30
+ api.every(INTERVAL_MS, async () => {
31
+ if (purchased >= MAX_PURCHASES) {
32
+ api.log.info(`DCA complete: ${purchased} purchases, $${totalSpent.toFixed(2)} spent, ${totalAcquired.toFixed(6)} ${COIN} acquired`);
33
+ return;
34
+ }
35
+
36
+ const mids = await api.client.getAllMids();
37
+ const price = parseFloat(mids[COIN]);
38
+ if (!price) {
39
+ api.log.warn(`No price for ${COIN}, skipping`);
40
+ return;
41
+ }
42
+
43
+ const size = AMOUNT_USD / price;
44
+ api.log.info(`[${purchased + 1}/${MAX_PURCHASES}] Buying ~$${AMOUNT_USD} of ${COIN} @ $${price.toFixed(2)}`);
45
+
46
+ const response = await api.client.marketOrder(COIN, true, size);
47
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
48
+ const status = response.response.data.statuses[0];
49
+ if (status?.filled) {
50
+ const filledSize = parseFloat(status.filled.totalSz);
51
+ const avgPx = parseFloat(status.filled.avgPx);
52
+ totalSpent += filledSize * avgPx;
53
+ totalAcquired += filledSize;
54
+ purchased++;
55
+
56
+ api.state.set('purchased', purchased);
57
+ api.state.set('totalSpent', totalSpent);
58
+ api.state.set('totalAcquired', totalAcquired);
59
+
60
+ const avgPrice = totalSpent / totalAcquired;
61
+ api.log.info(`Filled ${filledSize.toFixed(6)} @ $${avgPx.toFixed(2)} | Avg: $${avgPrice.toFixed(2)} | Total: ${totalAcquired.toFixed(6)} ${COIN}`);
62
+ } else if (status?.error) {
63
+ api.log.error(`Order failed: ${status.error}`);
64
+ }
65
+ }
66
+ });
67
+
68
+ api.onStop(() => {
69
+ const avgPrice = totalAcquired > 0 ? totalSpent / totalAcquired : 0;
70
+ api.log.info(`DCA stopped: ${purchased}/${MAX_PURCHASES} | Spent: $${totalSpent.toFixed(2)} | Acquired: ${totalAcquired.toFixed(6)} ${COIN} | Avg: $${avgPrice.toFixed(2)}`);
71
+ });
72
+ }
@@ -0,0 +1,98 @@
1
+ // Funding Arbitrage — Collect funding by positioning opposite to the crowd
2
+
3
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
4
+
5
+ export const config: AutomationConfig = {
6
+ description: 'Funding arbitrage — collect funding by positioning opposite to the crowd',
7
+ fields: {
8
+ coin: { type: 'string', description: 'Asset to trade', default: 'HYPE' },
9
+ sizeUsd: { type: 'number', description: 'Position size in USD notional', default: 5000 },
10
+ minFunding: { type: 'number', description: 'Min annualized % to enter', default: 20 },
11
+ maxFunding: { type: 'number', description: 'Max annualized % — avoid squeezes', default: 200 },
12
+ closeAt: { type: 'number', description: 'Close when funding drops below this %', default: 5 },
13
+ },
14
+ };
15
+
16
+ export default function fundingArb(api: AutomationAPI) {
17
+ const COIN = api.state.get<string>('coin', 'HYPE')!;
18
+ const SIZE_USD = api.state.get<number>('sizeUsd', 5000)!;
19
+ const MIN_FUNDING = api.state.get<number>('minFunding', 20)!;
20
+ const MAX_FUNDING = api.state.get<number>('maxFunding', 200)!;
21
+ const CLOSE_AT = api.state.get<number>('closeAt', 5)!;
22
+
23
+ let inPosition = api.state.get<boolean>('inPosition', false)!;
24
+ let positionSide = api.state.get<string>('positionSide', '')!;
25
+ let entryPrice = api.state.get<number>('entryPrice', 0)!;
26
+ let positionSize = api.state.get<number>('positionSize', 0)!;
27
+ let totalFunding = api.state.get<number>('totalFunding', 0)!;
28
+
29
+ api.onStart(() => {
30
+ api.log.info(`Funding arb: ${COIN} | $${SIZE_USD} | Enter >${MIN_FUNDING}% | Close <${CLOSE_AT}%`);
31
+ if (inPosition) {
32
+ api.log.info(`Resuming ${positionSide} position: ${positionSize.toFixed(6)} @ $${entryPrice.toFixed(2)}`);
33
+ }
34
+ });
35
+
36
+ api.on('funding_update', async ({ coin, annualized }) => {
37
+ if (coin !== COIN) return;
38
+
39
+ const annualizedPct = annualized * 100;
40
+ const absAnnualized = Math.abs(annualizedPct);
41
+
42
+ if (inPosition) {
43
+ // Check if we should close
44
+ const shouldClose =
45
+ (positionSide === 'short' && annualizedPct < CLOSE_AT) ||
46
+ (positionSide === 'long' && annualizedPct > -CLOSE_AT);
47
+
48
+ if (shouldClose) {
49
+ api.log.info(`Funding dropped to ${annualizedPct.toFixed(2)}% (below ${CLOSE_AT}%), closing ${positionSide}`);
50
+ const closeIsBuy = positionSide === 'short';
51
+ await api.client.marketOrder(coin, closeIsBuy, positionSize);
52
+
53
+ inPosition = false;
54
+ api.state.set('inPosition', false);
55
+ api.log.info(`Position closed. Funding collected: ~$${totalFunding.toFixed(2)}`);
56
+ } else {
57
+ api.log.debug(`${coin} funding: ${annualizedPct.toFixed(2)}% — holding ${positionSide}`);
58
+ }
59
+ return;
60
+ }
61
+
62
+ // Not in position — check if we should enter
63
+ if (absAnnualized >= MIN_FUNDING && absAnnualized <= MAX_FUNDING) {
64
+ const shouldShort = annualizedPct > 0; // Positive = longs pay shorts
65
+ const side = shouldShort ? 'short' : 'long';
66
+
67
+ const mids = await api.client.getAllMids();
68
+ const price = parseFloat(mids[coin]);
69
+ const size = SIZE_USD / price;
70
+
71
+ api.log.info(`Funding at ${annualizedPct.toFixed(2)}% — opening ${side} ${size.toFixed(6)} ${coin}`);
72
+ const response = await api.client.marketOrder(coin, !shouldShort, size);
73
+
74
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
75
+ const status = response.response.data.statuses[0];
76
+ if (status?.filled) {
77
+ positionSize = parseFloat(status.filled.totalSz);
78
+ entryPrice = parseFloat(status.filled.avgPx);
79
+ positionSide = side;
80
+ inPosition = true;
81
+
82
+ api.state.set('inPosition', true);
83
+ api.state.set('positionSide', side);
84
+ api.state.set('entryPrice', entryPrice);
85
+ api.state.set('positionSize', positionSize);
86
+
87
+ api.log.info(`Entered ${side} ${positionSize.toFixed(6)} @ $${entryPrice.toFixed(2)}`);
88
+ }
89
+ }
90
+ }
91
+ });
92
+
93
+ api.onStop(() => {
94
+ if (inPosition) {
95
+ api.log.warn(`Stopping with open ${positionSide} position of ${positionSize.toFixed(6)} ${COIN} — close manually if desired`);
96
+ }
97
+ });
98
+ }