openbroker 1.0.67 → 1.0.69

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 =
@@ -1344,23 +1344,34 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1344
1344
 
1345
1345
  {
1346
1346
  name: 'ob_auto_run',
1347
- description: 'Start a trading automation script. Scripts are TypeScript files that export a default factory function with event handlers (price_change, funding_update, position_opened, etc.). Scripts are loaded from ~/.openbroker/automations/ or an absolute path.',
1347
+ description: 'Start a trading automation script. Scripts are TypeScript files that export a default factory function with event handlers (price_change, funding_update, position_opened, etc.). Scripts are loaded from ~/.openbroker/automations/, an absolute path, or bundled examples (dca, grid, funding-arb, mm-spread, mm-maker).',
1348
1348
  parameters: {
1349
1349
  type: 'object',
1350
1350
  properties: {
1351
1351
  script: { type: 'string', description: 'Script name (from ~/.openbroker/automations/) or absolute path' },
1352
+ example: { type: 'string', description: 'Bundled example name: dca, grid, funding-arb, mm-spread, mm-maker' },
1353
+ config: { type: 'object', description: 'Key-value config to pre-seed automation state (e.g. { coin: "BTC", amount: 50 })' },
1352
1354
  id: { type: 'string', description: 'Custom automation ID (default: filename)' },
1353
1355
  dry: { type: 'boolean', description: 'Intercept write methods — no real trades' },
1354
1356
  poll: { type: 'number', description: 'Poll interval in milliseconds (default: 10000)' },
1355
1357
  },
1356
- required: ['script'],
1357
1358
  },
1358
1359
  async execute(_id, params) {
1359
1360
  try {
1360
- const { resolveScriptPath } = await import('../auto/loader.js');
1361
+ const { resolveScriptPath, resolveExamplePath } = await import('../auto/loader.js');
1361
1362
  const { startAutomation } = await import('../auto/runtime.js');
1362
1363
 
1363
- const scriptPath = resolveScriptPath(String(params.script));
1364
+ if (!params.script && !params.example) {
1365
+ return error('Either "script" or "example" parameter is required');
1366
+ }
1367
+
1368
+ const scriptPath = params.example
1369
+ ? resolveExamplePath(String(params.example))
1370
+ : resolveScriptPath(String(params.script));
1371
+ const initialState = params.config && typeof params.config === 'object'
1372
+ ? params.config as Record<string, unknown>
1373
+ : undefined;
1374
+
1364
1375
  const automation = await startAutomation({
1365
1376
  scriptPath,
1366
1377
  id: params.id ? String(params.id) : undefined,
@@ -1368,6 +1379,7 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1368
1379
  pollIntervalMs: params.poll ? Number(params.poll) : 10_000,
1369
1380
  gatewayPort,
1370
1381
  hooksToken,
1382
+ initialState,
1371
1383
  });
1372
1384
 
1373
1385
  return json({
@@ -1409,13 +1421,14 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1409
1421
 
1410
1422
  {
1411
1423
  name: 'ob_auto_list',
1412
- description: 'List available automation scripts and running automations (including those started from other processes)',
1424
+ description: 'List available automation scripts, bundled examples (dca, grid, funding-arb, mm-spread, mm-maker), and running automations',
1413
1425
  parameters: { type: 'object', properties: {} },
1414
1426
  async execute() {
1415
- const { listAutomations } = await import('../auto/loader.js');
1427
+ const { listAutomations, loadExampleConfigs } = await import('../auto/loader.js');
1416
1428
  const { getRunningAutomations, getRegisteredAutomations } = await import('../auto/runtime.js');
1417
1429
 
1418
1430
  const available = listAutomations();
1431
+ const examples = await loadExampleConfigs();
1419
1432
 
1420
1433
  // In-process automations with live stats
1421
1434
  const inProcess = getRunningAutomations().map(a => ({
@@ -1443,7 +1456,7 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1443
1456
  source: 'other_process',
1444
1457
  }));
1445
1458
 
1446
- return json({ available, running: [...inProcess, ...external] });
1459
+ return json({ available, examples, running: [...inProcess, ...external] });
1447
1460
  },
1448
1461
  },
1449
1462
  ];