openbroker 1.9.4 → 1.9.6
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/CHANGELOG.md +5 -0
- package/README.md +21 -4
- package/SKILL.md +3 -3
- package/bin/cli.ts +4 -2
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +71 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/operations/advanced-orders.test.d.ts +2 -0
- package/dist/operations/advanced-orders.test.d.ts.map +1 -0
- package/dist/operations/advanced-orders.test.js +189 -0
- package/dist/operations/bracket.d.ts +22 -0
- package/dist/operations/bracket.d.ts.map +1 -1
- package/dist/operations/bracket.js +106 -73
- package/dist/operations/chase.d.ts +19 -0
- package/dist/operations/chase.d.ts.map +1 -1
- package/dist/operations/chase.js +125 -58
- package/dist/operations/execution.d.ts +53 -0
- package/dist/operations/execution.d.ts.map +1 -0
- package/dist/operations/execution.js +106 -0
- package/dist/operations/scale.d.ts +50 -1
- package/dist/operations/scale.d.ts.map +1 -1
- package/dist/operations/scale.js +143 -105
- package/dist/setup/install.js +100 -3
- package/dist/setup/package-catalog.d.ts +12 -0
- package/dist/setup/package-catalog.d.ts.map +1 -0
- package/dist/setup/package-catalog.js +36 -0
- package/dist/setup/package-catalog.test.d.ts +2 -0
- package/dist/setup/package-catalog.test.d.ts.map +1 -0
- package/dist/setup/package-catalog.test.js +31 -0
- package/package.json +5 -3
- package/scripts/core/client.ts +91 -0
- package/scripts/lib.ts +3 -0
- package/scripts/operations/advanced-orders.test.ts +209 -0
- package/scripts/operations/bracket.ts +128 -72
- package/scripts/operations/chase.ts +138 -61
- package/scripts/operations/execution.ts +138 -0
- package/scripts/operations/scale.ts +186 -131
- package/scripts/setup/install.ts +110 -3
- package/scripts/setup/package-catalog.test.ts +50 -0
- package/scripts/setup/package-catalog.ts +49 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { WebSocketManager } from '../core/ws.js';
|
|
2
|
+
import { sleep } from '../core/utils.js';
|
|
3
|
+
export class UserFillWatcher {
|
|
4
|
+
client;
|
|
5
|
+
fills = new Map();
|
|
6
|
+
seenFills = new Set();
|
|
7
|
+
ws;
|
|
8
|
+
ownsWs;
|
|
9
|
+
started = false;
|
|
10
|
+
sinceMs;
|
|
11
|
+
user;
|
|
12
|
+
onFill = (fill) => {
|
|
13
|
+
this.recordFill({
|
|
14
|
+
oid: fill.oid,
|
|
15
|
+
coin: fill.coin,
|
|
16
|
+
px: fill.px,
|
|
17
|
+
sz: fill.sz,
|
|
18
|
+
time: fill.time,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
constructor(client, options = {}) {
|
|
22
|
+
this.client = client;
|
|
23
|
+
this.ws = options.ws === undefined ? new WebSocketManager(client.verbose) : options.ws;
|
|
24
|
+
this.ownsWs = options.ws === undefined;
|
|
25
|
+
this.sinceMs = options.sinceMs ?? Date.now();
|
|
26
|
+
this.user = (options.user ?? client.address);
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.started)
|
|
30
|
+
return;
|
|
31
|
+
this.started = true;
|
|
32
|
+
if (!this.ws)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await this.ws.connect();
|
|
36
|
+
this.ws.on('userFill', this.onFill);
|
|
37
|
+
await this.ws.subscribeUserFills(this.user);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
this.ws.off('userFill', this.onFill);
|
|
41
|
+
if (this.ownsWs)
|
|
42
|
+
await this.ws.close().catch(() => { });
|
|
43
|
+
this.ws = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
if (!this.started)
|
|
48
|
+
return;
|
|
49
|
+
this.started = false;
|
|
50
|
+
if (!this.ws)
|
|
51
|
+
return;
|
|
52
|
+
this.ws.off('userFill', this.onFill);
|
|
53
|
+
if (this.ownsWs)
|
|
54
|
+
await this.ws.close().catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
getFilled(oid) {
|
|
57
|
+
return this.fills.get(oid) ?? { size: 0, notional: 0 };
|
|
58
|
+
}
|
|
59
|
+
async waitForFill(oid, targetSize, timeoutMs, options = {}) {
|
|
60
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
61
|
+
const pollMs = Math.max(250, options.pollMs ?? 1000);
|
|
62
|
+
while (Date.now() <= deadline) {
|
|
63
|
+
await this.refreshRestFills(options.coin);
|
|
64
|
+
const filled = this.getFilled(oid);
|
|
65
|
+
if (filled.size >= targetSize * 0.999)
|
|
66
|
+
return filled;
|
|
67
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
68
|
+
}
|
|
69
|
+
await this.refreshRestFills(options.coin);
|
|
70
|
+
return this.getFilled(oid);
|
|
71
|
+
}
|
|
72
|
+
async refreshRestFills(coin) {
|
|
73
|
+
try {
|
|
74
|
+
const fills = await this.client.getUserFills(this.user);
|
|
75
|
+
for (const fill of fills) {
|
|
76
|
+
this.recordFill(fill, coin);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// WebSocket is the primary path; REST polling is best-effort fallback.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
recordFill(fill, expectedCoin) {
|
|
84
|
+
if (expectedCoin && fill.coin !== expectedCoin)
|
|
85
|
+
return;
|
|
86
|
+
if (fill.time < this.sinceMs)
|
|
87
|
+
return;
|
|
88
|
+
const key = `${fill.oid}:${fill.time}:${fill.px}:${fill.sz}`;
|
|
89
|
+
if (this.seenFills.has(key))
|
|
90
|
+
return;
|
|
91
|
+
const size = parseFloat(fill.sz);
|
|
92
|
+
const price = parseFloat(fill.px);
|
|
93
|
+
if (!Number.isFinite(size) || size <= 0 || !Number.isFinite(price) || price <= 0)
|
|
94
|
+
return;
|
|
95
|
+
this.seenFills.add(key);
|
|
96
|
+
const prev = this.fills.get(fill.oid) ?? { size: 0, notional: 0 };
|
|
97
|
+
const next = {
|
|
98
|
+
size: prev.size + size,
|
|
99
|
+
notional: prev.notional + size * price,
|
|
100
|
+
};
|
|
101
|
+
this.fills.set(fill.oid, {
|
|
102
|
+
...next,
|
|
103
|
+
avgPrice: next.notional / next.size,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,3 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
|
-
|
|
2
|
+
import type { CancelResponse, OrderResponse } from '../core/types.js';
|
|
3
|
+
export interface OrderLevel {
|
|
4
|
+
level: number;
|
|
5
|
+
price: number;
|
|
6
|
+
size: number;
|
|
7
|
+
distanceFromMid: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ScaleClient {
|
|
10
|
+
verbose: boolean;
|
|
11
|
+
getAllMids(): Promise<Record<string, string>>;
|
|
12
|
+
bulkOrder(orders: Array<{
|
|
13
|
+
coin: string;
|
|
14
|
+
isBuy: boolean;
|
|
15
|
+
size: number;
|
|
16
|
+
price: number;
|
|
17
|
+
tif?: 'Gtc' | 'Alo';
|
|
18
|
+
reduceOnly?: boolean;
|
|
19
|
+
leverage?: number;
|
|
20
|
+
}>): Promise<OrderResponse>;
|
|
21
|
+
bulkCancel(cancels: Array<{
|
|
22
|
+
coin: string;
|
|
23
|
+
oid: number;
|
|
24
|
+
}>): Promise<CancelResponse>;
|
|
25
|
+
}
|
|
26
|
+
export interface ScaleOptions {
|
|
27
|
+
coin: string;
|
|
28
|
+
side: 'buy' | 'sell';
|
|
29
|
+
size: number;
|
|
30
|
+
levels: number;
|
|
31
|
+
rangePct: number;
|
|
32
|
+
distribution?: 'linear' | 'exponential' | 'flat';
|
|
33
|
+
leverage?: number;
|
|
34
|
+
reduceOnly?: boolean;
|
|
35
|
+
tif?: 'Gtc' | 'Alo';
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
verbose?: boolean;
|
|
38
|
+
rollbackOnPartial?: boolean;
|
|
39
|
+
client?: ScaleClient;
|
|
40
|
+
output?: (line: string) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface ScaleResult {
|
|
43
|
+
status: 'dry' | 'complete' | 'partial' | 'failed';
|
|
44
|
+
levels: OrderLevel[];
|
|
45
|
+
restingOids: number[];
|
|
46
|
+
filledOids: number[];
|
|
47
|
+
errors: string[];
|
|
48
|
+
rolledBack: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function calculateLevels(midPrice: number, isBuy: boolean, totalSize: number, numLevels: number, rangePct: number, distribution: 'linear' | 'exponential' | 'flat'): OrderLevel[];
|
|
51
|
+
export declare function runScale(opts: ScaleOptions): Promise<ScaleResult>;
|
|
3
52
|
//# sourceMappingURL=scale.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../scripts/operations/scale.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../scripts/operations/scale.ts"],"names":[],"mappings":";AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAyCtE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,SAAS,CACP,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GACzI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1B,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAClD,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,GAC9C,UAAU,EAAE,CA0Cd;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA6HvE"}
|
package/dist/operations/scale.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
// Scale In/Out - Place a grid of limit orders
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import { getClient } from '../core/client.js';
|
|
4
|
-
import { formatUsd, parseArgs
|
|
5
|
+
import { formatUsd, parseArgs } from '../core/utils.js';
|
|
5
6
|
function printUsage() {
|
|
6
7
|
console.log(`
|
|
7
8
|
Open Broker - Scale In/Out
|
|
@@ -39,7 +40,7 @@ Examples:
|
|
|
39
40
|
npx tsx scripts/operations/scale.ts --coin ETH --side buy --size 2 --levels 8 --range 5 --distribution exponential
|
|
40
41
|
`);
|
|
41
42
|
}
|
|
42
|
-
function calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution) {
|
|
43
|
+
export function calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution) {
|
|
43
44
|
const levels = [];
|
|
44
45
|
// Calculate weights based on distribution
|
|
45
46
|
let weights = [];
|
|
@@ -75,6 +76,127 @@ function calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distri
|
|
|
75
76
|
}
|
|
76
77
|
return levels;
|
|
77
78
|
}
|
|
79
|
+
export async function runScale(opts) {
|
|
80
|
+
const out = opts.output ?? ((line) => console.log(line));
|
|
81
|
+
const distribution = opts.distribution ?? 'linear';
|
|
82
|
+
const reduceOnly = opts.reduceOnly ?? false;
|
|
83
|
+
const tif = opts.tif ?? 'Gtc';
|
|
84
|
+
const rollbackOnPartial = opts.rollbackOnPartial ?? true;
|
|
85
|
+
const isBuy = opts.side === 'buy';
|
|
86
|
+
if (!opts.coin)
|
|
87
|
+
throw new Error('coin is required');
|
|
88
|
+
if (opts.side !== 'buy' && opts.side !== 'sell')
|
|
89
|
+
throw new Error('side must be buy or sell');
|
|
90
|
+
if (!Number.isFinite(opts.size) || opts.size <= 0)
|
|
91
|
+
throw new Error('size must be positive');
|
|
92
|
+
if (!Number.isInteger(opts.levels) || opts.levels <= 0)
|
|
93
|
+
throw new Error('levels must be a positive integer');
|
|
94
|
+
if (!Number.isFinite(opts.rangePct) || opts.rangePct <= 0)
|
|
95
|
+
throw new Error('rangePct must be positive');
|
|
96
|
+
if (!['linear', 'exponential', 'flat'].includes(distribution))
|
|
97
|
+
throw new Error('distribution must be linear, exponential, or flat');
|
|
98
|
+
const client = opts.client ?? getClient();
|
|
99
|
+
if (opts.verbose)
|
|
100
|
+
client.verbose = true;
|
|
101
|
+
out('Open Broker - Scale In/Out');
|
|
102
|
+
out('==========================\n');
|
|
103
|
+
const mids = await client.getAllMids();
|
|
104
|
+
const midPrice = parseFloat(mids[opts.coin]);
|
|
105
|
+
if (!midPrice)
|
|
106
|
+
throw new Error(`No market data for ${opts.coin}`);
|
|
107
|
+
const levels = calculateLevels(midPrice, isBuy, opts.size, opts.levels, opts.rangePct, distribution);
|
|
108
|
+
const notional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
|
|
109
|
+
const avgPrice = notional / opts.size;
|
|
110
|
+
out('Order Plan');
|
|
111
|
+
out('----------');
|
|
112
|
+
out(`Coin: ${opts.coin}`);
|
|
113
|
+
out(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
114
|
+
out(`Total Size: ${opts.size}`);
|
|
115
|
+
out(`Current Mid: ${formatUsd(midPrice)}`);
|
|
116
|
+
out(`Levels: ${opts.levels}`);
|
|
117
|
+
out(`Range: ${opts.rangePct}% ${isBuy ? 'below' : 'above'} mid`);
|
|
118
|
+
out(`Distribution: ${distribution}`);
|
|
119
|
+
out(`Time in Force: ${tif}`);
|
|
120
|
+
out(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
|
|
121
|
+
out(`Est. Notional: ${formatUsd(notional)}`);
|
|
122
|
+
out(`Avg Price: ${formatUsd(avgPrice)}`);
|
|
123
|
+
out('\nOrder Grid');
|
|
124
|
+
out('----------');
|
|
125
|
+
out('Level | Price | Size | Distance');
|
|
126
|
+
out('------|--------------|------------|----------');
|
|
127
|
+
for (const level of levels) {
|
|
128
|
+
out(` ${level.level.toString().padStart(2)} | ` +
|
|
129
|
+
`${formatUsd(level.price).padStart(12)} | ` +
|
|
130
|
+
`${level.size.toFixed(6).padStart(10)} | ` +
|
|
131
|
+
`${level.distanceFromMid.toFixed(2)}%`);
|
|
132
|
+
}
|
|
133
|
+
if (opts.dryRun) {
|
|
134
|
+
out('\n🔍 Dry run - orders not placed');
|
|
135
|
+
return { status: 'dry', levels, restingOids: [], filledOids: [], errors: [], rolledBack: false };
|
|
136
|
+
}
|
|
137
|
+
out('\nPlacing ladder as a bulk order...\n');
|
|
138
|
+
const response = await client.bulkOrder(levels.map((level) => ({
|
|
139
|
+
coin: opts.coin,
|
|
140
|
+
isBuy,
|
|
141
|
+
size: level.size,
|
|
142
|
+
price: level.price,
|
|
143
|
+
tif,
|
|
144
|
+
reduceOnly,
|
|
145
|
+
leverage: opts.leverage,
|
|
146
|
+
})));
|
|
147
|
+
const restingOids = [];
|
|
148
|
+
const filledOids = [];
|
|
149
|
+
const errors = [];
|
|
150
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
151
|
+
response.response.data.statuses.forEach((status, index) => {
|
|
152
|
+
const level = levels[index];
|
|
153
|
+
if (status?.resting) {
|
|
154
|
+
restingOids.push(status.resting.oid);
|
|
155
|
+
out(`Level ${level.level}: ✅ OID ${status.resting.oid}`);
|
|
156
|
+
}
|
|
157
|
+
else if (status?.filled) {
|
|
158
|
+
filledOids.push(status.filled.oid);
|
|
159
|
+
out(`Level ${level.level}: ✅ Filled ${status.filled.totalSz} @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
|
|
160
|
+
}
|
|
161
|
+
else if (status?.error) {
|
|
162
|
+
errors.push(`Level ${level.level}: ${status.error}`);
|
|
163
|
+
out(`Level ${level.level}: ❌ ${status.error}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
errors.push(`Level ${level.level}: Unknown status`);
|
|
167
|
+
out(`Level ${level.level}: ⚠️ Unknown status`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const reason = typeof response.response === 'string' ? response.response : 'Bulk order failed';
|
|
173
|
+
errors.push(reason);
|
|
174
|
+
out(`❌ ${reason}`);
|
|
175
|
+
}
|
|
176
|
+
let rolledBack = false;
|
|
177
|
+
if (errors.length > 0 && rollbackOnPartial && restingOids.length > 0) {
|
|
178
|
+
out('\nPartial ladder placement detected; cancelling resting ladder orders...');
|
|
179
|
+
await client.bulkCancel(restingOids.map((oid) => ({ coin: opts.coin, oid })));
|
|
180
|
+
rolledBack = true;
|
|
181
|
+
out(`Cancelled ${restingOids.length} resting order(s).`);
|
|
182
|
+
}
|
|
183
|
+
out('\n========== Summary ==========');
|
|
184
|
+
out(`Orders Placed: ${restingOids.length + filledOids.length}/${opts.levels}`);
|
|
185
|
+
if (errors.length > 0)
|
|
186
|
+
out(`Failed: ${errors.length}`);
|
|
187
|
+
if (restingOids.length > 0)
|
|
188
|
+
out(`Resting OIDs: ${restingOids.join(', ')}`);
|
|
189
|
+
if (filledOids.length > 0)
|
|
190
|
+
out(`Filled OIDs: ${filledOids.join(', ')}`);
|
|
191
|
+
if (rolledBack)
|
|
192
|
+
out('Rollback: Resting orders cancelled');
|
|
193
|
+
const status = errors.length === 0
|
|
194
|
+
? 'complete'
|
|
195
|
+
: restingOids.length + filledOids.length > 0
|
|
196
|
+
? 'partial'
|
|
197
|
+
: 'failed';
|
|
198
|
+
return { status, levels, restingOids, filledOids, errors, rolledBack };
|
|
199
|
+
}
|
|
78
200
|
async function main() {
|
|
79
201
|
const args = parseArgs(process.argv.slice(2));
|
|
80
202
|
const coin = args.coin;
|
|
@@ -91,15 +213,6 @@ async function main() {
|
|
|
91
213
|
printUsage();
|
|
92
214
|
process.exit(1);
|
|
93
215
|
}
|
|
94
|
-
if (side !== 'buy' && side !== 'sell') {
|
|
95
|
-
console.error('Error: --side must be "buy" or "sell"');
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
if (!['linear', 'exponential', 'flat'].includes(distribution)) {
|
|
99
|
-
console.error('Error: --distribution must be linear, exponential, or flat');
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
// Map uppercase CLI input to Pascal case for SDK
|
|
103
216
|
const tifMap = {
|
|
104
217
|
'GTC': 'Gtc',
|
|
105
218
|
'ALO': 'Alo'
|
|
@@ -109,104 +222,29 @@ async function main() {
|
|
|
109
222
|
console.error('Error: --tif must be GTC or ALO');
|
|
110
223
|
process.exit(1);
|
|
111
224
|
}
|
|
112
|
-
const isBuy = side === 'buy';
|
|
113
|
-
const client = getClient();
|
|
114
|
-
if (args.verbose) {
|
|
115
|
-
client.verbose = true;
|
|
116
|
-
}
|
|
117
|
-
console.log('Open Broker - Scale In/Out');
|
|
118
|
-
console.log('==========================\n');
|
|
119
225
|
try {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
226
|
+
const result = await runScale({
|
|
227
|
+
coin,
|
|
228
|
+
side: side,
|
|
229
|
+
size: totalSize,
|
|
230
|
+
levels: numLevels,
|
|
231
|
+
rangePct,
|
|
232
|
+
distribution,
|
|
233
|
+
leverage,
|
|
234
|
+
reduceOnly,
|
|
235
|
+
tif,
|
|
236
|
+
dryRun,
|
|
237
|
+
verbose: args.verbose,
|
|
238
|
+
});
|
|
239
|
+
if (result.status === 'failed')
|
|
124
240
|
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
const levels = calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution);
|
|
127
|
-
const notional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
|
|
128
|
-
const avgPrice = notional / totalSize;
|
|
129
|
-
console.log('Order Plan');
|
|
130
|
-
console.log('----------');
|
|
131
|
-
console.log(`Coin: ${coin}`);
|
|
132
|
-
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
133
|
-
console.log(`Total Size: ${totalSize}`);
|
|
134
|
-
console.log(`Current Mid: ${formatUsd(midPrice)}`);
|
|
135
|
-
console.log(`Levels: ${numLevels}`);
|
|
136
|
-
console.log(`Range: ${rangePct}% ${isBuy ? 'below' : 'above'} mid`);
|
|
137
|
-
console.log(`Distribution: ${distribution}`);
|
|
138
|
-
console.log(`Time in Force: ${tif}`);
|
|
139
|
-
console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
|
|
140
|
-
console.log(`Est. Notional: ${formatUsd(notional)}`);
|
|
141
|
-
console.log(`Avg Price: ${formatUsd(avgPrice)}`);
|
|
142
|
-
console.log('\nOrder Grid');
|
|
143
|
-
console.log('----------');
|
|
144
|
-
console.log('Level | Price | Size | Distance');
|
|
145
|
-
console.log('------|--------------|------------|----------');
|
|
146
|
-
for (const level of levels) {
|
|
147
|
-
console.log(` ${level.level.toString().padStart(2)} | ` +
|
|
148
|
-
`${formatUsd(level.price).padStart(12)} | ` +
|
|
149
|
-
`${level.size.toFixed(6).padStart(10)} | ` +
|
|
150
|
-
`${level.distanceFromMid.toFixed(2)}%`);
|
|
151
|
-
}
|
|
152
|
-
if (dryRun) {
|
|
153
|
-
console.log('\n🔍 Dry run - orders not placed');
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
console.log('\nPlacing orders...\n');
|
|
157
|
-
const results = [];
|
|
158
|
-
for (const level of levels) {
|
|
159
|
-
process.stdout.write(`Level ${level.level}: ${formatUsd(level.price)} x ${level.size.toFixed(6)}... `);
|
|
160
|
-
try {
|
|
161
|
-
const response = await client.limitOrder(coin, isBuy, level.size, level.price, tif, reduceOnly, leverage);
|
|
162
|
-
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
163
|
-
const status = response.response.data.statuses[0];
|
|
164
|
-
if (status?.resting) {
|
|
165
|
-
console.log(`✅ OID: ${status.resting.oid}`);
|
|
166
|
-
results.push({ level: level.level, oid: status.resting.oid });
|
|
167
|
-
}
|
|
168
|
-
else if (status?.filled) {
|
|
169
|
-
console.log(`✅ Filled immediately @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
|
|
170
|
-
results.push({ level: level.level, oid: status.filled.oid });
|
|
171
|
-
}
|
|
172
|
-
else if (status?.error) {
|
|
173
|
-
console.log(`❌ ${status.error}`);
|
|
174
|
-
results.push({ level: level.level, error: status.error });
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
console.log(`⚠️ Unknown status`);
|
|
178
|
-
results.push({ level: level.level, error: 'Unknown status' });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
const error = typeof response.response === 'string' ? response.response : 'Failed';
|
|
183
|
-
console.log(`❌ ${error}`);
|
|
184
|
-
results.push({ level: level.level, error });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
189
|
-
console.log(`❌ ${error}`);
|
|
190
|
-
results.push({ level: level.level, error });
|
|
191
|
-
}
|
|
192
|
-
// Small delay between orders
|
|
193
|
-
await sleep(100);
|
|
194
|
-
}
|
|
195
|
-
// Summary
|
|
196
|
-
const successful = results.filter(r => r.oid).length;
|
|
197
|
-
const failed = results.filter(r => r.error).length;
|
|
198
|
-
console.log('\n========== Summary ==========');
|
|
199
|
-
console.log(`Orders Placed: ${successful}/${numLevels}`);
|
|
200
|
-
if (failed > 0) {
|
|
201
|
-
console.log(`Failed: ${failed}`);
|
|
202
|
-
}
|
|
203
|
-
if (successful > 0) {
|
|
204
|
-
console.log(`Order IDs: ${results.filter(r => r.oid).map(r => r.oid).join(', ')}`);
|
|
205
|
-
}
|
|
206
241
|
}
|
|
207
242
|
catch (error) {
|
|
208
|
-
console.error('Error:', error);
|
|
243
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
209
244
|
process.exit(1);
|
|
210
245
|
}
|
|
211
246
|
}
|
|
212
|
-
|
|
247
|
+
// Only run when invoked as a script — not when imported as a module.
|
|
248
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
249
|
+
main();
|
|
250
|
+
}
|
package/dist/setup/install.js
CHANGED
|
@@ -5,19 +5,54 @@ import * as path from 'path';
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
|
+
import { INSTALLABLE_PACKAGES, packageSpec, resolveInstallablePackage, } from './package-catalog.js';
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
10
11
|
const packageRoot = path.resolve(__dirname, '../..');
|
|
11
|
-
const
|
|
12
|
+
const rawArgs = process.argv.slice(2);
|
|
13
|
+
const args = new Set(rawArgs);
|
|
14
|
+
function positionalArgs() {
|
|
15
|
+
const positionals = [];
|
|
16
|
+
for (let index = 0; index < rawArgs.length; index++) {
|
|
17
|
+
const arg = rawArgs[index];
|
|
18
|
+
if (arg === '--tag') {
|
|
19
|
+
index++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!arg.startsWith('-'))
|
|
23
|
+
positionals.push(arg);
|
|
24
|
+
}
|
|
25
|
+
return positionals;
|
|
26
|
+
}
|
|
27
|
+
function optionValue(flag) {
|
|
28
|
+
const index = rawArgs.indexOf(flag);
|
|
29
|
+
if (index < 0)
|
|
30
|
+
return null;
|
|
31
|
+
const value = rawArgs[index + 1];
|
|
32
|
+
if (!value || value.startsWith('-'))
|
|
33
|
+
fail(`${flag} requires a value`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
12
36
|
function printUsage() {
|
|
13
37
|
console.log(`
|
|
14
|
-
OpenBroker
|
|
15
|
-
|
|
38
|
+
OpenBroker Installer
|
|
39
|
+
====================
|
|
16
40
|
|
|
17
41
|
Usage:
|
|
42
|
+
openbroker install <package> [--tag <version>] [--dry]
|
|
43
|
+
openbroker install --list
|
|
18
44
|
openbroker install --codex [options]
|
|
19
45
|
npx openbroker@latest install --codex [options]
|
|
20
46
|
|
|
47
|
+
Companion packages:
|
|
48
|
+
monitoring Install the local automation dashboard
|
|
49
|
+
extended Install the Extended Exchange CLI
|
|
50
|
+
|
|
51
|
+
Package options:
|
|
52
|
+
--tag <tag> Install a release tag or exact version (default: latest)
|
|
53
|
+
--dry Print the npm command without installing
|
|
54
|
+
--list List supported companion packages
|
|
55
|
+
|
|
21
56
|
Harnesses:
|
|
22
57
|
--codex Install the OpenBroker skill for Codex
|
|
23
58
|
|
|
@@ -74,6 +109,55 @@ function installGlobalCli() {
|
|
|
74
109
|
fail('global CLI installation failed. Fix the npm permission error, then rerun with --skip-cli.');
|
|
75
110
|
}
|
|
76
111
|
}
|
|
112
|
+
function printInstallablePackages() {
|
|
113
|
+
console.log('Installable OpenBroker packages:\n');
|
|
114
|
+
for (const entry of INSTALLABLE_PACKAGES) {
|
|
115
|
+
console.log(` ${entry.key.padEnd(12)} ${entry.packageName.padEnd(26)} ${entry.description}`);
|
|
116
|
+
}
|
|
117
|
+
console.log('\nInstall or upgrade with: openbroker install <package>');
|
|
118
|
+
}
|
|
119
|
+
function installCompanionPackage(target) {
|
|
120
|
+
const entry = resolveInstallablePackage(target);
|
|
121
|
+
if (!entry) {
|
|
122
|
+
printInstallablePackages();
|
|
123
|
+
fail(`unknown installable package: ${target}`);
|
|
124
|
+
}
|
|
125
|
+
const allowedFlags = new Set(['--tag', '--dry']);
|
|
126
|
+
const unsupported = rawArgs.filter((arg, index) => (arg.startsWith('-')
|
|
127
|
+
&& !allowedFlags.has(arg)
|
|
128
|
+
&& rawArgs[index - 1] !== '--tag'));
|
|
129
|
+
if (unsupported.length > 0)
|
|
130
|
+
fail(`unsupported package option: ${unsupported[0]}`);
|
|
131
|
+
const tag = optionValue('--tag') ?? 'latest';
|
|
132
|
+
let spec;
|
|
133
|
+
try {
|
|
134
|
+
spec = packageSpec(entry, tag);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
138
|
+
}
|
|
139
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
140
|
+
const npmArgs = ['install', '--global', spec];
|
|
141
|
+
console.log(`OpenBroker — Install ${entry.key}`);
|
|
142
|
+
console.log('================================\n');
|
|
143
|
+
console.log(`Package: ${spec}`);
|
|
144
|
+
console.log(`Command: ${npmCommand} ${npmArgs.join(' ')}`);
|
|
145
|
+
if (args.has('--dry')) {
|
|
146
|
+
console.log('\nDry run only; nothing was installed.');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const result = spawnSync(npmCommand, npmArgs, { stdio: 'inherit' });
|
|
150
|
+
if (result.error)
|
|
151
|
+
fail(`could not start npm: ${result.error.message}`);
|
|
152
|
+
if (result.status !== 0) {
|
|
153
|
+
fail(`installation failed for ${entry.packageName}; resolve the npm error and retry`);
|
|
154
|
+
}
|
|
155
|
+
console.log(`\n✅ ${entry.packageName} installed successfully.`);
|
|
156
|
+
console.log(`Available command: ${entry.command}`);
|
|
157
|
+
console.log('\nNext steps:');
|
|
158
|
+
for (const step of entry.nextSteps)
|
|
159
|
+
console.log(` ${step}`);
|
|
160
|
+
}
|
|
77
161
|
function runApiWalletSetup() {
|
|
78
162
|
const onboardPath = path.join(packageRoot, 'scripts', 'setup', 'onboard.ts');
|
|
79
163
|
console.log('\nStarting restricted API-wallet onboarding...\n');
|
|
@@ -94,6 +178,19 @@ function main() {
|
|
|
94
178
|
printUsage();
|
|
95
179
|
return;
|
|
96
180
|
}
|
|
181
|
+
if (args.has('--list')) {
|
|
182
|
+
printInstallablePackages();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const positionals = positionalArgs();
|
|
186
|
+
if (positionals.length > 0) {
|
|
187
|
+
if (positionals.length > 1)
|
|
188
|
+
fail(`expected one package name, received: ${positionals.join(' ')}`);
|
|
189
|
+
if (args.has('--codex'))
|
|
190
|
+
fail('choose either a companion package or the --codex harness installer');
|
|
191
|
+
installCompanionPackage(positionals[0]);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
97
194
|
if (!args.has('--codex')) {
|
|
98
195
|
printUsage();
|
|
99
196
|
fail('choose a supported harness flag such as --codex');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface InstallablePackage {
|
|
2
|
+
key: string;
|
|
3
|
+
aliases: string[];
|
|
4
|
+
packageName: string;
|
|
5
|
+
command: string;
|
|
6
|
+
description: string;
|
|
7
|
+
nextSteps: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare const INSTALLABLE_PACKAGES: InstallablePackage[];
|
|
10
|
+
export declare function resolveInstallablePackage(input: string): InstallablePackage | null;
|
|
11
|
+
export declare function packageSpec(entry: InstallablePackage, tag?: string): string;
|
|
12
|
+
//# sourceMappingURL=package-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-catalog.d.ts","sourceRoot":"","sources":["../../scripts/setup/package-catalog.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,eAAO,MAAM,oBAAoB,EAAE,kBAAkB,EAuBpD,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAOlF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,SAAW,GAAG,MAAM,CAK7E"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const INSTALLABLE_PACKAGES = [
|
|
2
|
+
{
|
|
3
|
+
key: 'monitoring',
|
|
4
|
+
aliases: ['openbroker-monitoring'],
|
|
5
|
+
packageName: 'openbroker-monitoring',
|
|
6
|
+
command: 'openbroker-monitoring',
|
|
7
|
+
description: 'Local automation dashboard and optional audit observer',
|
|
8
|
+
nextSteps: [
|
|
9
|
+
'openbroker-monitoring serve --host 127.0.0.1 --port 3001',
|
|
10
|
+
'Open http://127.0.0.1:3001',
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: 'extended',
|
|
15
|
+
aliases: ['openbroker-extended'],
|
|
16
|
+
packageName: 'openbroker-extended',
|
|
17
|
+
command: 'openbroker-ex',
|
|
18
|
+
description: 'Extended Exchange trading CLI and library',
|
|
19
|
+
nextSteps: [
|
|
20
|
+
'openbroker-ex --help',
|
|
21
|
+
'openbroker-ex setup',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
export function resolveInstallablePackage(input) {
|
|
26
|
+
const normalized = input.trim().toLowerCase();
|
|
27
|
+
return INSTALLABLE_PACKAGES.find((entry) => (entry.key === normalized
|
|
28
|
+
|| entry.packageName === normalized
|
|
29
|
+
|| entry.aliases.includes(normalized))) ?? null;
|
|
30
|
+
}
|
|
31
|
+
export function packageSpec(entry, tag = 'latest') {
|
|
32
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(tag)) {
|
|
33
|
+
throw new Error(`invalid package tag or version: ${tag}`);
|
|
34
|
+
}
|
|
35
|
+
return `${entry.packageName}@${tag}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-catalog.test.d.ts","sourceRoot":"","sources":["../../scripts/setup/package-catalog.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { INSTALLABLE_PACKAGES, packageSpec, resolveInstallablePackage, } from './package-catalog.js';
|
|
6
|
+
const installScript = fileURLToPath(new URL('./install.ts', import.meta.url));
|
|
7
|
+
test('resolves companion packages by short name and npm package name', () => {
|
|
8
|
+
assert.equal(resolveInstallablePackage('monitoring')?.packageName, 'openbroker-monitoring');
|
|
9
|
+
assert.equal(resolveInstallablePackage('openbroker-monitoring')?.key, 'monitoring');
|
|
10
|
+
assert.equal(resolveInstallablePackage('EXTENDED')?.command, 'openbroker-ex');
|
|
11
|
+
assert.equal(resolveInstallablePackage('unknown'), null);
|
|
12
|
+
});
|
|
13
|
+
test('builds pinned or latest npm package specs without accepting arbitrary specs', () => {
|
|
14
|
+
const monitoring = INSTALLABLE_PACKAGES.find((entry) => entry.key === 'monitoring');
|
|
15
|
+
assert.ok(monitoring);
|
|
16
|
+
assert.equal(packageSpec(monitoring), 'openbroker-monitoring@latest');
|
|
17
|
+
assert.equal(packageSpec(monitoring, '1.4.2'), 'openbroker-monitoring@1.4.2');
|
|
18
|
+
assert.throws(() => packageSpec(monitoring, 'npm:unrelated-package'));
|
|
19
|
+
assert.throws(() => packageSpec(monitoring, '../local-package'));
|
|
20
|
+
});
|
|
21
|
+
test('installer dry run prints the global npm operation without writing', () => {
|
|
22
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', installScript, 'monitoring', '--tag', '1.4.2', '--dry'], { encoding: 'utf8' });
|
|
23
|
+
assert.equal(result.status, 0, result.stderr);
|
|
24
|
+
assert.match(result.stdout, /npm install --global openbroker-monitoring@1\.4\.2/);
|
|
25
|
+
assert.match(result.stdout, /nothing was installed/i);
|
|
26
|
+
});
|
|
27
|
+
test('installer rejects packages outside the catalog', () => {
|
|
28
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', installScript, 'unrelated-package', '--dry'], { encoding: 'utf8' });
|
|
29
|
+
assert.equal(result.status, 1);
|
|
30
|
+
assert.match(result.stderr, /unknown installable package/i);
|
|
31
|
+
});
|