openbroker 1.0.66 → 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.
@@ -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
+ }
@@ -0,0 +1,135 @@
1
+ // Grid Trading — Place buy/sell orders at evenly spaced price levels
2
+
3
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
4
+
5
+ export const config: AutomationConfig = {
6
+ description: 'Grid trading — buy/sell orders at evenly spaced price levels',
7
+ fields: {
8
+ coin: { type: 'string', description: 'Asset to trade', default: 'HYPE' },
9
+ lower: { type: 'number', description: 'Lower price bound (default: auto -5% from mid)', default: 0 },
10
+ upper: { type: 'number', description: 'Upper price bound (default: auto +5% from mid)', default: 0 },
11
+ grids: { type: 'number', description: 'Number of grid levels', default: 10 },
12
+ size: { type: 'number', description: 'Size per level in base asset', default: 0.1 },
13
+ mode: { type: 'string', description: 'Grid mode: neutral, long, or short', default: 'neutral' },
14
+ },
15
+ };
16
+
17
+ interface GridLevel {
18
+ price: number;
19
+ side: 'buy' | 'sell';
20
+ size: number;
21
+ oid?: number;
22
+ }
23
+
24
+ export default function grid(api: AutomationAPI) {
25
+ const COIN = api.state.get<string>('coin', 'HYPE')!;
26
+ const GRIDS = api.state.get<number>('grids', 10)!;
27
+ const SIZE = api.state.get<number>('size', 0.1)!;
28
+ const MODE = api.state.get<string>('mode', 'neutral')!;
29
+
30
+ let levels: GridLevel[] = [];
31
+ let realizedPnl = api.state.get<number>('realizedPnl', 0)!;
32
+ let initialized = false;
33
+
34
+ api.onStart(async () => {
35
+ const mids = await api.client.getAllMids();
36
+ const mid = parseFloat(mids[COIN]);
37
+ if (!mid) {
38
+ api.log.error(`No price for ${COIN}`);
39
+ return;
40
+ }
41
+
42
+ const lower = api.state.get<number>('lower', mid * 0.95)!;
43
+ const upper = api.state.get<number>('upper', mid * 1.05)!;
44
+ const spacing = (upper - lower) / (GRIDS - 1);
45
+
46
+ api.log.info(`Grid: ${COIN} ${api.utils.formatUsd(lower)}-${api.utils.formatUsd(upper)} | ${GRIDS} levels | ${SIZE}/level | ${MODE}`);
47
+
48
+ // Build and place grid
49
+ for (let i = 0; i < GRIDS; i++) {
50
+ const price = lower + spacing * i;
51
+ let side: 'buy' | 'sell';
52
+ if (MODE === 'long') side = 'buy';
53
+ else if (MODE === 'short') side = 'sell';
54
+ else side = price < mid ? 'buy' : 'sell';
55
+
56
+ // Skip levels too close to mid
57
+ if (Math.abs(price - mid) / mid < 0.001) continue;
58
+
59
+ const level: GridLevel = { price, side, size: SIZE };
60
+
61
+ const response = await api.client.limitOrder(COIN, side === 'buy', SIZE, price, 'Gtc', false);
62
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
63
+ const status = response.response.data.statuses[0];
64
+ if (status?.resting) {
65
+ level.oid = status.resting.oid;
66
+ api.log.info(`${side.toUpperCase()} @ ${api.utils.formatUsd(price)} — OID: ${level.oid}`);
67
+ } else if (status?.filled) {
68
+ api.log.info(`${side.toUpperCase()} @ ${api.utils.formatUsd(price)} — filled immediately`);
69
+ }
70
+ }
71
+
72
+ levels.push(level);
73
+ await api.utils.sleep(100);
74
+ }
75
+
76
+ initialized = true;
77
+ api.log.info(`Grid initialized: ${levels.filter(l => l.oid).length} open orders`);
78
+ });
79
+
80
+ // Monitor fills and replace with opposite orders
81
+ api.on('tick', async () => {
82
+ if (!initialized || levels.length === 0) return;
83
+
84
+ const openOrders = await api.client.getOpenOrders();
85
+ const openOids = new Set(openOrders.filter(o => o.coin === COIN).map(o => o.oid));
86
+
87
+ const mids = await api.client.getAllMids();
88
+ const mid = parseFloat(mids[COIN]);
89
+ const lower = api.state.get<number>('lower', mid * 0.95)!;
90
+ const upper = api.state.get<number>('upper', mid * 1.05)!;
91
+ const spacing = (upper - lower) / (GRIDS - 1);
92
+
93
+ for (const level of levels) {
94
+ if (!level.oid || openOids.has(level.oid)) continue;
95
+
96
+ // Order was filled
97
+ api.log.info(`${level.side.toUpperCase()} FILLED @ ${api.utils.formatUsd(level.price)}`);
98
+ level.oid = undefined;
99
+
100
+ if (MODE !== 'neutral') continue;
101
+
102
+ // Place opposite order
103
+ const oppositeSide = level.side === 'buy' ? 'sell' : 'buy';
104
+ const oppositePrice = level.side === 'buy' ? level.price + spacing : level.price - spacing;
105
+
106
+ if (oppositePrice < lower || oppositePrice > upper) continue;
107
+
108
+ const response = await api.client.limitOrder(COIN, oppositeSide === 'buy', SIZE, oppositePrice, 'Gtc', false);
109
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
110
+ const status = response.response.data.statuses[0];
111
+ if (status?.resting) {
112
+ const newLevel: GridLevel = { price: oppositePrice, side: oppositeSide, size: SIZE, oid: status.resting.oid };
113
+ levels.push(newLevel);
114
+
115
+ if (level.side === 'buy') {
116
+ realizedPnl += (oppositePrice - level.price) * SIZE;
117
+ }
118
+
119
+ api.state.set('realizedPnl', realizedPnl);
120
+ api.log.info(`Placed ${oppositeSide.toUpperCase()} @ ${api.utils.formatUsd(oppositePrice)} | PnL: ${api.utils.formatUsd(realizedPnl)}`);
121
+ }
122
+ }
123
+ }
124
+ });
125
+
126
+ api.onStop(async () => {
127
+ api.log.info('Cancelling grid orders...');
128
+ for (const level of levels) {
129
+ if (level.oid) {
130
+ try { await api.client.cancel(COIN, level.oid); } catch { /* may be filled */ }
131
+ }
132
+ }
133
+ api.log.info(`Grid stopped. Realized PnL: ${api.utils.formatUsd(realizedPnl)}`);
134
+ });
135
+ }
@@ -0,0 +1,125 @@
1
+ // Maker-Only Market Making — ALO orders that guarantee maker rebates
2
+
3
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
4
+
5
+ export const config: AutomationConfig = {
6
+ description: 'Maker-only market making — ALO orders for guaranteed maker rebates',
7
+ fields: {
8
+ coin: { type: 'string', description: 'Asset to market make', default: 'HYPE' },
9
+ size: { type: 'number', description: 'Order size on each side (base asset)', default: 0.1 },
10
+ offsetBps: { type: 'number', description: 'Offset from best bid/ask in bps', default: 1 },
11
+ maxPosition: { type: 'number', description: 'Max net position (default: 3x size)', default: 0.3 },
12
+ skewFactor: { type: 'number', description: 'Inventory skew aggressiveness', default: 2.0 },
13
+ },
14
+ };
15
+
16
+ export default function mmMaker(api: AutomationAPI) {
17
+ const COIN = api.state.get<string>('coin', 'HYPE')!;
18
+ const SIZE = api.state.get<number>('size', 0.1)!;
19
+ const OFFSET_BPS = api.state.get<number>('offsetBps', 1)!;
20
+ const MAX_POS = api.state.get<number>('maxPosition', SIZE * 3)!;
21
+ const SKEW = api.state.get<number>('skewFactor', 2.0)!;
22
+
23
+ let bidOid: number | undefined;
24
+ let askOid: number | undefined;
25
+ let bidPrice = 0;
26
+ let askPrice = 0;
27
+ let totalBought = 0;
28
+ let totalSold = 0;
29
+ let totalBuyCost = 0;
30
+ let totalSellRevenue = 0;
31
+ let rejections = 0;
32
+
33
+ const offsetFraction = OFFSET_BPS / 10000;
34
+
35
+ api.onStart(() => {
36
+ api.log.info(`Maker MM: ${COIN} | ${SIZE}/side | ${OFFSET_BPS}bps offset | ALO only`);
37
+ });
38
+
39
+ api.on('tick', async () => {
40
+ const book = await api.client.getL2Book(COIN);
41
+ if (book.bestBid === 0 || book.bestAsk === 0) return;
42
+
43
+ // Get position
44
+ const userState = await api.client.getUserState();
45
+ const pos = userState.assetPositions.find(p => p.position.coin === COIN);
46
+ const position = pos ? parseFloat(pos.position.szi) : 0;
47
+
48
+ // Check fills
49
+ const openOrders = await api.client.getOpenOrders();
50
+ const openOids = new Set(openOrders.filter(o => o.coin === COIN).map(o => o.oid));
51
+
52
+ if (bidOid && !openOids.has(bidOid)) {
53
+ totalBought += SIZE;
54
+ totalBuyCost += bidPrice * SIZE;
55
+ api.log.info(`BID FILLED @ ${api.utils.formatUsd(bidPrice)} | Pos: ${position.toFixed(4)} | +rebate`);
56
+ bidOid = undefined;
57
+ }
58
+ if (askOid && !openOids.has(askOid)) {
59
+ totalSold += SIZE;
60
+ totalSellRevenue += askPrice * SIZE;
61
+ api.log.info(`ASK FILLED @ ${api.utils.formatUsd(askPrice)} | Pos: ${position.toFixed(4)} | +rebate`);
62
+ askOid = undefined;
63
+ }
64
+
65
+ // Inventory skew
66
+ const ratio = Math.max(-1, Math.min(1, position / MAX_POS));
67
+ const bidSkewMult = 1 + ratio * SKEW;
68
+ const askSkewMult = 1 - ratio * SKEW;
69
+
70
+ const targetBid = book.bestBid * (1 - offsetFraction * Math.max(0.1, bidSkewMult));
71
+ const targetAsk = book.bestAsk * (1 + offsetFraction * Math.max(0.1, askSkewMult));
72
+
73
+ // Ensure no crossing
74
+ const safeBid = Math.min(targetBid, book.bestAsk * 0.9999);
75
+ const safeAsk = Math.max(targetAsk, book.bestBid * 1.0001);
76
+
77
+ const shouldBid = position < MAX_POS;
78
+ const shouldAsk = position > -MAX_POS;
79
+
80
+ // Cancel stale quotes
81
+ if (bidOid) {
82
+ const drift = Math.abs(bidPrice - safeBid) / book.midPrice;
83
+ if (drift > 0.0005 || !shouldBid) {
84
+ try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
85
+ bidOid = undefined;
86
+ }
87
+ }
88
+ if (askOid) {
89
+ const drift = Math.abs(askPrice - safeAsk) / book.midPrice;
90
+ if (drift > 0.0005 || !shouldAsk) {
91
+ try { await api.client.cancel(COIN, askOid); } catch { /* */ }
92
+ askOid = undefined;
93
+ }
94
+ }
95
+
96
+ // Place ALO bid
97
+ if (shouldBid && !bidOid && safeBid < book.bestAsk) {
98
+ const resp = await api.client.limitOrder(COIN, true, SIZE, safeBid, 'Alo', false);
99
+ if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
100
+ const s = resp.response.data.statuses[0];
101
+ if (s?.resting) { bidOid = s.resting.oid; bidPrice = safeBid; }
102
+ else if (s?.error) { rejections++; }
103
+ }
104
+ }
105
+
106
+ // Place ALO ask
107
+ if (shouldAsk && !askOid && safeAsk > book.bestBid) {
108
+ const resp = await api.client.limitOrder(COIN, false, SIZE, safeAsk, 'Alo', false);
109
+ if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
110
+ const s = resp.response.data.statuses[0];
111
+ if (s?.resting) { askOid = s.resting.oid; askPrice = safeAsk; }
112
+ else if (s?.error) { rejections++; }
113
+ }
114
+ }
115
+ });
116
+
117
+ api.onStop(async () => {
118
+ if (bidOid) try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
119
+ if (askOid) try { await api.client.cancel(COIN, askOid); } catch { /* */ }
120
+ const pnl = totalSellRevenue - totalBuyCost;
121
+ const volume = totalBuyCost + totalSellRevenue;
122
+ const rebates = volume * 0.00003;
123
+ api.log.info(`Maker MM stopped. PnL: ${api.utils.formatUsd(pnl)} | Rebates: ~${api.utils.formatUsd(rebates)} | Rejections: ${rejections}`);
124
+ });
125
+ }
@@ -0,0 +1,115 @@
1
+ // Market Making (Spread) — Quote bid/ask around mid with inventory skewing
2
+
3
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
4
+
5
+ export const config: AutomationConfig = {
6
+ description: 'Market making — quote bid/ask around mid price with inventory skewing',
7
+ fields: {
8
+ coin: { type: 'string', description: 'Asset to market make', default: 'HYPE' },
9
+ size: { type: 'number', description: 'Order size on each side (base asset)', default: 0.1 },
10
+ spreadBps: { type: 'number', description: 'Spread in bps from mid price', default: 10 },
11
+ maxPosition: { type: 'number', description: 'Max net position before pausing side (default: 3x size)', default: 0.3 },
12
+ skewFactor: { type: 'number', description: 'Inventory skew aggressiveness', default: 2.0 },
13
+ },
14
+ };
15
+
16
+ export default function mmSpread(api: AutomationAPI) {
17
+ const COIN = api.state.get<string>('coin', 'HYPE')!;
18
+ const SIZE = api.state.get<number>('size', 0.1)!;
19
+ const SPREAD_BPS = api.state.get<number>('spreadBps', 10)!;
20
+ const MAX_POS = api.state.get<number>('maxPosition', SIZE * 3)!;
21
+ const SKEW = api.state.get<number>('skewFactor', 2.0)!;
22
+
23
+ let bidOid: number | undefined;
24
+ let askOid: number | undefined;
25
+ let bidPrice = 0;
26
+ let askPrice = 0;
27
+ let totalBought = 0;
28
+ let totalSold = 0;
29
+ let totalBuyCost = 0;
30
+ let totalSellRevenue = 0;
31
+
32
+ const halfSpread = SPREAD_BPS / 10000 / 2;
33
+
34
+ api.onStart(() => {
35
+ api.log.info(`MM Spread: ${COIN} | ${SIZE}/side | ${SPREAD_BPS}bps | Max: ±${MAX_POS}`);
36
+ });
37
+
38
+ api.on('tick', async () => {
39
+ const mids = await api.client.getAllMids();
40
+ const mid = parseFloat(mids[COIN]);
41
+ if (!mid) return;
42
+
43
+ // Get position
44
+ const userState = await api.client.getUserState();
45
+ const pos = userState.assetPositions.find(p => p.position.coin === COIN);
46
+ const position = pos ? parseFloat(pos.position.szi) : 0;
47
+
48
+ // Check fills
49
+ const openOrders = await api.client.getOpenOrders();
50
+ const openOids = new Set(openOrders.filter(o => o.coin === COIN).map(o => o.oid));
51
+
52
+ if (bidOid && !openOids.has(bidOid)) {
53
+ totalBought += SIZE;
54
+ totalBuyCost += bidPrice * SIZE;
55
+ api.log.info(`BID FILLED @ ${api.utils.formatUsd(bidPrice)} | Pos: ${position.toFixed(4)}`);
56
+ bidOid = undefined;
57
+ }
58
+ if (askOid && !openOids.has(askOid)) {
59
+ totalSold += SIZE;
60
+ totalSellRevenue += askPrice * SIZE;
61
+ api.log.info(`ASK FILLED @ ${api.utils.formatUsd(askPrice)} | Pos: ${position.toFixed(4)}`);
62
+ askOid = undefined;
63
+ }
64
+
65
+ // Inventory skew
66
+ const ratio = Math.max(-1, Math.min(1, position / MAX_POS));
67
+ const bidSkew = halfSpread * (1 + ratio * SKEW);
68
+ const askSkew = halfSpread * (1 - ratio * SKEW);
69
+
70
+ const targetBid = mid * (1 - Math.max(0.0001, bidSkew));
71
+ const targetAsk = mid * (1 + Math.max(0.0001, askSkew));
72
+
73
+ const shouldBid = position < MAX_POS;
74
+ const shouldAsk = position > -MAX_POS;
75
+
76
+ // Cancel stale quotes
77
+ if (bidOid) {
78
+ const drift = Math.abs(bidPrice - targetBid) / mid;
79
+ if (drift > 0.001 || !shouldBid) {
80
+ try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
81
+ bidOid = undefined;
82
+ }
83
+ }
84
+ if (askOid) {
85
+ const drift = Math.abs(askPrice - targetAsk) / mid;
86
+ if (drift > 0.001 || !shouldAsk) {
87
+ try { await api.client.cancel(COIN, askOid); } catch { /* */ }
88
+ askOid = undefined;
89
+ }
90
+ }
91
+
92
+ // Place new quotes
93
+ if (shouldBid && !bidOid) {
94
+ const resp = await api.client.limitOrder(COIN, true, SIZE, targetBid, 'Gtc', false);
95
+ if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
96
+ const s = resp.response.data.statuses[0];
97
+ if (s?.resting) { bidOid = s.resting.oid; bidPrice = targetBid; }
98
+ }
99
+ }
100
+ if (shouldAsk && !askOid) {
101
+ const resp = await api.client.limitOrder(COIN, false, SIZE, targetAsk, 'Gtc', false);
102
+ if (resp.status === 'ok' && resp.response && typeof resp.response === 'object') {
103
+ const s = resp.response.data.statuses[0];
104
+ if (s?.resting) { askOid = s.resting.oid; askPrice = targetAsk; }
105
+ }
106
+ }
107
+ });
108
+
109
+ api.onStop(async () => {
110
+ if (bidOid) try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
111
+ if (askOid) try { await api.client.cancel(COIN, askOid); } catch { /* */ }
112
+ const pnl = totalSellRevenue - totalBuyCost;
113
+ api.log.info(`MM stopped. Bought: ${totalBought.toFixed(6)} | Sold: ${totalSold.toFixed(6)} | PnL: ${api.utils.formatUsd(pnl)}`);
114
+ });
115
+ }
@@ -3,9 +3,14 @@
3
3
  import { existsSync, readdirSync, mkdirSync } from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
- import type { AutomationFactory } from './types.js';
6
+ import { fileURLToPath } from 'url';
7
+ import type { AutomationFactory, AutomationConfig } from './types.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
7
11
 
8
12
  const AUTOMATIONS_DIR = path.join(os.homedir(), '.openbroker', 'automations');
13
+ const EXAMPLES_DIR = path.join(__dirname, 'examples');
9
14
 
10
15
  /** Resolve a script path from a name or path */
11
16
  export function resolveScriptPath(nameOrPath: string): string {
@@ -35,6 +40,47 @@ export function resolveScriptPath(nameOrPath: string): string {
35
40
  );
36
41
  }
37
42
 
43
+ /** Resolve a bundled example by name */
44
+ export function resolveExamplePath(name: string): string {
45
+ const examplePath = path.join(EXAMPLES_DIR, `${name}.ts`);
46
+ if (!existsSync(examplePath)) {
47
+ const available = listExamples().map(e => e.name).join(', ');
48
+ throw new Error(`Unknown example: ${name}\nAvailable: ${available}`);
49
+ }
50
+ return examplePath;
51
+ }
52
+
53
+ /** List bundled example automations */
54
+ export function listExamples(): Array<{ name: string; path: string }> {
55
+ if (!existsSync(EXAMPLES_DIR)) return [];
56
+
57
+ return readdirSync(EXAMPLES_DIR)
58
+ .filter(f => f.endsWith('.ts') && !f.startsWith('.'))
59
+ .map(f => ({
60
+ name: f.replace(/\.ts$/, ''),
61
+ path: path.join(EXAMPLES_DIR, f),
62
+ }));
63
+ }
64
+
65
+ /** Load config metadata from all bundled examples */
66
+ export async function loadExampleConfigs(): Promise<Record<string, AutomationConfig>> {
67
+ const examples = listExamples();
68
+ const configs: Record<string, AutomationConfig> = {};
69
+
70
+ for (const example of examples) {
71
+ try {
72
+ const mod = await import(example.path);
73
+ if (mod.config && typeof mod.config === 'object' && mod.config.description) {
74
+ configs[example.name] = mod.config as AutomationConfig;
75
+ }
76
+ } catch {
77
+ // Skip examples that fail to load
78
+ }
79
+ }
80
+
81
+ return configs;
82
+ }
83
+
38
84
  /** Load an automation module and validate the default export */
39
85
  export async function loadAutomation(scriptPath: string): Promise<AutomationFactory> {
40
86
  const absolutePath = path.resolve(scriptPath);
@@ -231,6 +231,8 @@ export interface RuntimeOptions {
231
231
  gatewayPort?: number;
232
232
  /** Hooks token for webhook auth. Falls back to OPENCLAW_HOOKS_TOKEN */
233
233
  hooksToken?: string;
234
+ /** Pre-seed state before the factory function runs (e.g. from --set key=value) */
235
+ initialState?: Record<string, unknown>;
234
236
  }
235
237
 
236
238
  /** Registry of all running automations */
@@ -255,6 +257,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
255
257
  pollIntervalMs = 10_000,
256
258
  gatewayPort,
257
259
  hooksToken,
260
+ initialState,
258
261
  } = options;
259
262
 
260
263
  const id = options.id || path.basename(scriptPath, '.ts');
@@ -265,6 +268,16 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
265
268
 
266
269
  const log = createLogger(id, verbose);
267
270
  const state = createState(id);
271
+
272
+ // Pre-seed state from --set flags (doesn't overwrite already-persisted keys)
273
+ if (initialState) {
274
+ for (const [key, value] of Object.entries(initialState)) {
275
+ if (state.get(key) === undefined) {
276
+ state.set(key, value);
277
+ }
278
+ }
279
+ }
280
+
268
281
  const eventBus = new AutomationEventBus();
269
282
 
270
283
  const rawClient = getClient();
@@ -8,6 +8,20 @@ import type { HyperliquidClient } from '../core/client.js';
8
8
  /** What an automation .ts file exports */
9
9
  export type AutomationFactory = (api: AutomationAPI) => void | Promise<void>;
10
10
 
11
+ /** Config field descriptor for example automations */
12
+ export interface AutomationConfigField {
13
+ type: 'string' | 'number' | 'boolean';
14
+ description: string;
15
+ default: unknown;
16
+ required?: boolean;
17
+ }
18
+
19
+ /** Config metadata exported by example automations as `export const config` */
20
+ export interface AutomationConfig {
21
+ description: string;
22
+ fields: Record<string, AutomationConfigField>;
23
+ }
24
+
11
25
  // ── Event system ────────────────────────────────────────────────────
12
26
 
13
27
  export type AutomationEventType =
@@ -11,11 +11,14 @@ Usage: openbroker orders [options]
11
11
  Options:
12
12
  --coin <symbol> Filter by coin (e.g. ETH, BTC)
13
13
  --status <status> Filter by status (filled, canceled, open, triggered, rejected, etc.)
14
+ --open Show only currently open orders
14
15
  --top <n> Show last N orders (default: 20)
15
16
  --help, -h Show this help
16
17
 
17
18
  Examples:
18
19
  openbroker orders
20
+ openbroker orders --open
21
+ openbroker orders --open --coin ETH
19
22
  openbroker orders --coin ETH --status filled
20
23
  openbroker orders --top 50
21
24
  `);
@@ -31,11 +34,78 @@ async function main() {
31
34
 
32
35
  const filterCoin = args.coin as string | undefined;
33
36
  const filterStatus = args.status as string | undefined;
37
+ const openOnly = args.open as boolean;
34
38
  const top = parseInt(args.top as string) || 20;
35
39
  const jsonOutput = args.json as boolean;
36
40
  const client = getClient();
37
41
 
38
42
  try {
43
+ if (openOnly) {
44
+ // Use the dedicated open orders endpoint
45
+ let openOrders = await client.getOpenOrders();
46
+
47
+ if (filterCoin) {
48
+ openOrders = openOrders.filter(o => o.coin === normalizeCoin(filterCoin));
49
+ }
50
+
51
+ openOrders.sort((a, b) => b.timestamp - a.timestamp);
52
+ openOrders = openOrders.slice(0, top);
53
+
54
+ if (jsonOutput) {
55
+ console.log(JSON.stringify(openOrders.map(o => ({
56
+ time: new Date(o.timestamp).toISOString(),
57
+ coin: o.coin,
58
+ side: o.side === 'B' ? 'buy' : 'sell',
59
+ orderType: o.orderType,
60
+ size: o.sz,
61
+ origSize: o.origSz,
62
+ price: o.limitPx,
63
+ status: 'open',
64
+ oid: o.oid,
65
+ })), null, 2));
66
+ return;
67
+ }
68
+
69
+ console.log('Open Broker - Open Orders');
70
+ console.log('=========================\n');
71
+
72
+ if (openOrders.length === 0) {
73
+ console.log('No open orders found');
74
+ return;
75
+ }
76
+
77
+ // Table header
78
+ console.log(
79
+ 'Time'.padEnd(20) +
80
+ 'Coin'.padEnd(10) +
81
+ 'Side'.padEnd(6) +
82
+ 'Type'.padEnd(14) +
83
+ 'Size'.padEnd(12) +
84
+ 'Price'.padEnd(14) +
85
+ 'OID'
86
+ );
87
+ console.log('─'.repeat(90));
88
+
89
+ for (const o of openOrders) {
90
+ const time = new Date(o.timestamp).toLocaleString();
91
+ const side = o.side === 'B' ? 'BUY' : 'SELL';
92
+
93
+ console.log(
94
+ time.padEnd(20) +
95
+ o.coin.padEnd(10) +
96
+ side.padEnd(6) +
97
+ o.orderType.padEnd(14) +
98
+ o.sz.padEnd(12) +
99
+ formatUsd(parseFloat(o.limitPx)).padEnd(14) +
100
+ String(o.oid)
101
+ );
102
+ }
103
+
104
+ console.log('─'.repeat(90));
105
+ console.log(`Showing ${openOrders.length} open orders`);
106
+ return;
107
+ }
108
+
39
109
  let orders = await client.getHistoricalOrders();
40
110
 
41
111
  if (filterCoin) {