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