openbroker 1.0.89 ā 1.1.0
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/package.json +6 -6
- package/scripts/auto/cli.ts +0 -2
- package/scripts/auto/runtime.ts +0 -4
- package/scripts/lib.ts +80 -0
- package/scripts/operations/bracket.ts +216 -202
- package/scripts/operations/chase.ts +184 -166
- package/SKILL.md +0 -1182
- package/openclaw.plugin.json +0 -86
- package/scripts/plugin/cli.ts +0 -127
- package/scripts/plugin/config-bridge.ts +0 -30
- package/scripts/plugin/index.ts +0 -133
- package/scripts/plugin/tools.ts +0 -1686
- package/scripts/plugin/types.ts +0 -158
- package/scripts/plugin/watcher.ts +0 -321
|
@@ -27,210 +27,228 @@ Options:
|
|
|
27
27
|
--reduce Reduce-only order
|
|
28
28
|
--dry Dry run - show chase parameters without executing
|
|
29
29
|
|
|
30
|
-
Strategy:
|
|
31
|
-
- Places ALO (post-only) order at mid ± offset
|
|
32
|
-
- If not filled, cancels and replaces closer to mid
|
|
33
|
-
- Stops when filled or timeout/max-chase reached
|
|
34
|
-
- Uses ALO to ensure maker rebates
|
|
35
|
-
|
|
36
30
|
Examples:
|
|
37
31
|
# Chase buy 0.5 ETH with 5 bps offset, 5 min timeout
|
|
38
32
|
npx tsx scripts/operations/chase.ts --coin ETH --side buy --size 0.5
|
|
39
33
|
|
|
40
34
|
# Chase sell with tighter offset and longer timeout
|
|
41
35
|
npx tsx scripts/operations/chase.ts --coin BTC --side sell --size 0.1 --offset 2 --timeout 600
|
|
42
|
-
|
|
43
|
-
# Quick aggressive chase (1 bps offset, 1 min timeout, 50 bps max chase)
|
|
44
|
-
npx tsx scripts/operations/chase.ts --coin SOL --side buy --size 10 --offset 1 --timeout 60 --max-chase 50
|
|
45
36
|
`);
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
export interface ChaseOptions {
|
|
40
|
+
coin: string;
|
|
41
|
+
side: 'buy' | 'sell';
|
|
42
|
+
size: number;
|
|
43
|
+
offsetBps?: number;
|
|
44
|
+
timeoutSec?: number;
|
|
45
|
+
intervalMs?: number;
|
|
46
|
+
maxChaseBps?: number;
|
|
47
|
+
leverage?: number;
|
|
48
|
+
reduceOnly?: boolean;
|
|
49
|
+
dryRun?: boolean;
|
|
50
|
+
verbose?: boolean;
|
|
51
|
+
/** Receives each output line. Defaults to console.log. */
|
|
52
|
+
output?: (line: string) => void;
|
|
53
|
+
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const leverage = args.leverage ? parseInt(args.leverage as string) : undefined;
|
|
59
|
-
const reduceOnly = args.reduce as boolean;
|
|
60
|
-
const dryRun = args.dry as boolean;
|
|
55
|
+
export interface ChaseResult {
|
|
56
|
+
status: 'dry' | 'filled' | 'timeout' | 'max_chase_exceeded';
|
|
57
|
+
iterations: number;
|
|
58
|
+
durationSec: number;
|
|
59
|
+
startMid: number;
|
|
60
|
+
endMid: number;
|
|
61
|
+
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
export async function runChase(opts: ChaseOptions): Promise<ChaseResult> {
|
|
64
|
+
const out = opts.output ?? ((line: string) => console.log(line));
|
|
65
|
+
const offsetBps = opts.offsetBps ?? 5;
|
|
66
|
+
const timeoutSec = opts.timeoutSec ?? 300;
|
|
67
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
68
|
+
const maxChaseBps = opts.maxChaseBps ?? 100;
|
|
69
|
+
const isBuy = opts.side === 'buy';
|
|
66
70
|
|
|
67
|
-
if (
|
|
68
|
-
console.error('Error: --side must be "buy" or "sell"');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
+
if (opts.size <= 0 || isNaN(opts.size)) throw new Error('size must be positive');
|
|
71
72
|
|
|
72
|
-
const isBuy = side === 'buy';
|
|
73
73
|
const client = getClient();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
if (opts.verbose) client.verbose = true;
|
|
75
|
+
|
|
76
|
+
out('Open Broker - Chase Order');
|
|
77
|
+
out('=========================\n');
|
|
78
|
+
|
|
79
|
+
const mids = await client.getAllMids();
|
|
80
|
+
const startMid = parseFloat(mids[opts.coin]);
|
|
81
|
+
if (!startMid) throw new Error(`No market data for ${opts.coin}`);
|
|
82
|
+
|
|
83
|
+
const maxChasePrice = isBuy
|
|
84
|
+
? startMid * (1 + maxChaseBps / 10000)
|
|
85
|
+
: startMid * (1 - maxChaseBps / 10000);
|
|
86
|
+
|
|
87
|
+
out('Chase Parameters');
|
|
88
|
+
out('----------------');
|
|
89
|
+
out(`Coin: ${opts.coin}`);
|
|
90
|
+
out(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
91
|
+
out(`Size: ${opts.size}`);
|
|
92
|
+
out(`Start Mid: ${formatUsd(startMid)}`);
|
|
93
|
+
out(`Offset: ${offsetBps} bps (${(offsetBps / 100).toFixed(2)}%)`);
|
|
94
|
+
out(`Max Chase: ${maxChaseBps} bps to ${formatUsd(maxChasePrice)}`);
|
|
95
|
+
out(`Timeout: ${timeoutSec}s`);
|
|
96
|
+
out(`Update Rate: ${intervalMs}ms`);
|
|
97
|
+
out(`Order Type: ALO (post-only)`);
|
|
98
|
+
|
|
99
|
+
if (opts.dryRun) {
|
|
100
|
+
out('\nš Dry run - chase not started');
|
|
101
|
+
return { status: 'dry', iterations: 0, durationSec: 0, startMid, endMid: startMid };
|
|
77
102
|
}
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
console.log('=========================\n');
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const mids = await client.getAllMids();
|
|
84
|
-
const startMid = parseFloat(mids[coin]);
|
|
85
|
-
if (!startMid) {
|
|
86
|
-
console.error(`Error: No market data for ${coin}`);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
104
|
+
out('\nChasing...\n');
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(`Coin: ${coin}`);
|
|
97
|
-
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
98
|
-
console.log(`Size: ${size}`);
|
|
99
|
-
console.log(`Start Mid: ${formatUsd(startMid)}`);
|
|
100
|
-
console.log(`Offset: ${offsetBps} bps (${(offsetBps / 100).toFixed(2)}%)`);
|
|
101
|
-
console.log(`Max Chase: ${maxChaseBps} bps to ${formatUsd(maxChasePrice)}`);
|
|
102
|
-
console.log(`Timeout: ${timeoutSec}s`);
|
|
103
|
-
console.log(`Update Rate: ${intervalMs}ms`);
|
|
104
|
-
console.log(`Order Type: ALO (post-only)`);
|
|
105
|
-
|
|
106
|
-
if (dryRun) {
|
|
107
|
-
console.log('\nš Dry run - chase not started');
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
let currentOid: number | null = null;
|
|
108
|
+
let lastPrice: number | null = null;
|
|
109
|
+
let iteration = 0;
|
|
110
|
+
let filled = false;
|
|
111
|
+
let exitReason: 'filled' | 'timeout' | 'max_chase_exceeded' = 'timeout';
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
while (Date.now() - startTime < timeoutSec * 1000) {
|
|
114
|
+
iteration++;
|
|
112
115
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
let lastPrice: number | null = null;
|
|
116
|
-
let iteration = 0;
|
|
117
|
-
let filled = false;
|
|
116
|
+
const currentMids = await client.getAllMids();
|
|
117
|
+
const currentMid = parseFloat(currentMids[opts.coin]);
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
if (isBuy && currentMid > maxChasePrice) {
|
|
120
|
+
out(`\nā ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
121
|
+
exitReason = 'max_chase_exceeded';
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (!isBuy && currentMid < maxChasePrice) {
|
|
125
|
+
out(`\nā ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
126
|
+
exitReason = 'max_chase_exceeded';
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
const orderPrice = isBuy
|
|
131
|
+
? currentMid * (1 - offsetBps / 10000)
|
|
132
|
+
: currentMid * (1 + offsetBps / 10000);
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
if (isBuy && currentMid > maxChasePrice) {
|
|
128
|
-
console.log(`\nā ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
if (!isBuy && currentMid < maxChasePrice) {
|
|
132
|
-
console.log(`\nā ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
134
|
+
const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
|
|
143
|
-
|
|
144
|
-
if (priceChanged) {
|
|
145
|
-
// Cancel existing order if any
|
|
146
|
-
if (currentOid !== null) {
|
|
147
|
-
try {
|
|
148
|
-
await client.cancel(coin, currentOid);
|
|
149
|
-
} catch {
|
|
150
|
-
// Order might have filled
|
|
151
|
-
}
|
|
152
|
-
currentOid = null;
|
|
136
|
+
if (priceChanged) {
|
|
137
|
+
if (currentOid !== null) {
|
|
138
|
+
try {
|
|
139
|
+
await client.cancel(opts.coin, currentOid);
|
|
140
|
+
} catch {
|
|
141
|
+
// Order might have filled
|
|
153
142
|
}
|
|
143
|
+
currentOid = null;
|
|
144
|
+
}
|
|
154
145
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
console.log(`\nā
Order filled!`);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
146
|
+
const orders = await client.getOpenOrders();
|
|
147
|
+
const ourOrder = orders.find(o => o.coin === opts.coin && o.oid === currentOid);
|
|
148
|
+
if (!ourOrder && currentOid !== null) {
|
|
149
|
+
const state = await client.getUserState();
|
|
150
|
+
const pos = state.assetPositions.find(p => p.position.coin === opts.coin);
|
|
151
|
+
if (pos && Math.abs(parseFloat(pos.position.szi)) >= opts.size * 0.99) {
|
|
152
|
+
filled = true;
|
|
153
|
+
exitReason = 'filled';
|
|
154
|
+
out(`\nā
Order filled!`);
|
|
155
|
+
break;
|
|
168
156
|
}
|
|
157
|
+
}
|
|
169
158
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// If ALO rejected (would be taker), try again next iteration
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
console.log(`ā Failed`);
|
|
159
|
+
out(`[${iteration}] Mid: ${formatUsd(currentMid)} ā Order: ${formatUsd(orderPrice)}...`);
|
|
160
|
+
|
|
161
|
+
const response = await client.limitOrder(opts.coin, isBuy, opts.size, orderPrice, 'Alo', opts.reduceOnly, opts.leverage);
|
|
162
|
+
|
|
163
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
164
|
+
const status = response.response.data.statuses[0];
|
|
165
|
+
if (status?.resting) {
|
|
166
|
+
currentOid = status.resting.oid;
|
|
167
|
+
lastPrice = orderPrice;
|
|
168
|
+
out(`OID: ${currentOid}`);
|
|
169
|
+
} else if (status?.filled) {
|
|
170
|
+
filled = true;
|
|
171
|
+
exitReason = 'filled';
|
|
172
|
+
out(`ā
Filled @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
|
|
173
|
+
break;
|
|
174
|
+
} else if (status?.error) {
|
|
175
|
+
out(`ā ${status.error}`);
|
|
191
176
|
}
|
|
192
177
|
} else {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
178
|
+
out(`ā Failed`);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
if (currentOid !== null) {
|
|
182
|
+
const orders = await client.getOpenOrders();
|
|
183
|
+
const ourOrder = orders.find(o => o.oid === currentOid);
|
|
184
|
+
if (!ourOrder) {
|
|
185
|
+
filled = true;
|
|
186
|
+
exitReason = 'filled';
|
|
187
|
+
out(`\nā
Order filled!`);
|
|
188
|
+
break;
|
|
202
189
|
}
|
|
203
|
-
process.stdout.write('.');
|
|
204
190
|
}
|
|
205
|
-
|
|
206
|
-
await sleep(intervalMs);
|
|
207
191
|
}
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
193
|
+
await sleep(intervalMs);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (currentOid !== null && !filled) {
|
|
197
|
+
out(`\nCancelling unfilled order...`);
|
|
198
|
+
try {
|
|
199
|
+
await client.cancel(opts.coin, currentOid);
|
|
200
|
+
out(`ā
Cancelled`);
|
|
201
|
+
} catch {
|
|
202
|
+
out(`ā ļø Could not cancel (may have filled)`);
|
|
218
203
|
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
207
|
+
const endMid = parseFloat((await client.getAllMids())[opts.coin]);
|
|
208
|
+
const priceMove = ((endMid - startMid) / startMid) * 10000;
|
|
219
209
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
210
|
+
out('\n========== Chase Summary ==========');
|
|
211
|
+
out(`Duration: ${elapsed.toFixed(1)}s`);
|
|
212
|
+
out(`Iterations: ${iteration}`);
|
|
213
|
+
out(`Start Mid: ${formatUsd(startMid)}`);
|
|
214
|
+
out(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
|
|
215
|
+
out(`Result: ${filled ? 'ā
Filled' : 'ā Not filled'}`);
|
|
224
216
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.log(`Iterations: ${iteration}`);
|
|
228
|
-
console.log(`Start Mid: ${formatUsd(startMid)}`);
|
|
229
|
-
console.log(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
|
|
230
|
-
console.log(`Result: ${filled ? 'ā
Filled' : 'ā Not filled'}`);
|
|
217
|
+
return { status: exitReason, iterations: iteration, durationSec: elapsed, startMid, endMid };
|
|
218
|
+
}
|
|
231
219
|
|
|
220
|
+
async function main() {
|
|
221
|
+
const args = parseArgs(process.argv.slice(2));
|
|
222
|
+
|
|
223
|
+
const coin = args.coin as string;
|
|
224
|
+
const side = args.side as string;
|
|
225
|
+
const size = parseFloat(args.size as string);
|
|
226
|
+
|
|
227
|
+
if (!coin || !side || isNaN(size)) {
|
|
228
|
+
printUsage();
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
232
|
+
console.error('Error: --side must be "buy" or "sell"');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await runChase({
|
|
238
|
+
coin,
|
|
239
|
+
side: side as 'buy' | 'sell',
|
|
240
|
+
size,
|
|
241
|
+
offsetBps: args.offset ? parseInt(args.offset as string) : undefined,
|
|
242
|
+
timeoutSec: args.timeout ? parseInt(args.timeout as string) : undefined,
|
|
243
|
+
intervalMs: args.interval ? parseInt(args.interval as string) : undefined,
|
|
244
|
+
maxChaseBps: args['max-chase'] ? parseInt(args['max-chase'] as string) : undefined,
|
|
245
|
+
leverage: args.leverage ? parseInt(args.leverage as string) : undefined,
|
|
246
|
+
reduceOnly: args.reduce as boolean,
|
|
247
|
+
dryRun: args.dry as boolean,
|
|
248
|
+
verbose: args.verbose as boolean,
|
|
249
|
+
});
|
|
232
250
|
} catch (error) {
|
|
233
|
-
console.error('Error:', error);
|
|
251
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
234
252
|
process.exit(1);
|
|
235
253
|
}
|
|
236
254
|
}
|