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
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbroker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"openbroker": "./bin/openbroker.js"
|
|
8
8
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
9
|
+
"main": "./scripts/lib.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./scripts/lib.ts",
|
|
12
|
+
"./package.json": "./package.json"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"bin/",
|
|
14
16
|
"scripts/",
|
|
15
17
|
"config/example.env",
|
|
16
|
-
"openclaw.plugin.json",
|
|
17
18
|
"README.md",
|
|
18
|
-
"CHANGELOG.md"
|
|
19
|
-
"SKILL.md"
|
|
19
|
+
"CHANGELOG.md"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"onboard": "tsx scripts/setup/onboard.ts",
|
package/scripts/auto/cli.ts
CHANGED
|
@@ -130,8 +130,6 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
|
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// Resolve OpenClaw gateway env vars here (no network code in this file)
|
|
134
|
-
// so the runtime stays clean of process.env reads next to fetch() calls.
|
|
135
133
|
const envHooksToken = process.env.OPENCLAW_HOOKS_TOKEN;
|
|
136
134
|
const envGatewayPortStr = process.env.OPENCLAW_GATEWAY_PORT;
|
|
137
135
|
const envGatewayPort = envGatewayPortStr ? parseInt(envGatewayPortStr, 10) : undefined;
|
package/scripts/auto/runtime.ts
CHANGED
|
@@ -349,10 +349,6 @@ function createPublish(
|
|
|
349
349
|
hooksToken?: string,
|
|
350
350
|
): (message: string, options?: PublishOptions) => Promise<boolean> {
|
|
351
351
|
return async (message: string, options?: PublishOptions): Promise<boolean> => {
|
|
352
|
-
// Token & port come exclusively from options. Env-var fallbacks live in
|
|
353
|
-
// the call sites (plugin/index.ts and auto/cli.ts), so the env reads
|
|
354
|
-
// aren't co-located with the fetch() below and don't trip the OpenClaw
|
|
355
|
-
// "credential harvesting" scanner rule.
|
|
356
352
|
const token = hooksToken;
|
|
357
353
|
const port = gatewayPort || 18789;
|
|
358
354
|
|
package/scripts/lib.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Public library API for `openbroker`.
|
|
2
|
+
//
|
|
3
|
+
// External packages — notably `openbroker-plugin` — import from here to
|
|
4
|
+
// drive the CLI's functionality in-process (no `child_process` dispatch).
|
|
5
|
+
//
|
|
6
|
+
// Stability: every symbol re-exported here is a public API. Renames or
|
|
7
|
+
// removals are breaking changes and require a major version bump.
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
HyperliquidClient,
|
|
11
|
+
getClient,
|
|
12
|
+
} from './core/client.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
loadConfig,
|
|
16
|
+
isConfigured,
|
|
17
|
+
getNetwork,
|
|
18
|
+
isMainnet,
|
|
19
|
+
ensureConfigDir,
|
|
20
|
+
getConfigPath,
|
|
21
|
+
GLOBAL_CONFIG_DIR,
|
|
22
|
+
GLOBAL_ENV_PATH,
|
|
23
|
+
OPEN_BROKER_BUILDER_ADDRESS,
|
|
24
|
+
} from './core/config.js';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
roundPrice,
|
|
28
|
+
roundSize,
|
|
29
|
+
sleep,
|
|
30
|
+
normalizeCoin,
|
|
31
|
+
formatUsd,
|
|
32
|
+
formatPercent,
|
|
33
|
+
annualizeFundingRate,
|
|
34
|
+
parseArgs,
|
|
35
|
+
getSlippagePrice,
|
|
36
|
+
getTimestampMs,
|
|
37
|
+
generateCloid,
|
|
38
|
+
orderToWire,
|
|
39
|
+
checkBuilderFeeApproval,
|
|
40
|
+
} from './core/utils.js';
|
|
41
|
+
|
|
42
|
+
export type * from './core/types.js';
|
|
43
|
+
|
|
44
|
+
// ── Operations (in-process callable) ────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export { runBracket } from './operations/bracket.js';
|
|
47
|
+
export type { BracketOptions, BracketResult } from './operations/bracket.js';
|
|
48
|
+
|
|
49
|
+
export { runChase } from './operations/chase.js';
|
|
50
|
+
export type { ChaseOptions, ChaseResult } from './operations/chase.js';
|
|
51
|
+
|
|
52
|
+
// ── Automation runtime ──────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
startAutomation,
|
|
56
|
+
getRunningAutomations,
|
|
57
|
+
getAutomation,
|
|
58
|
+
getRegisteredAutomations,
|
|
59
|
+
} from './auto/runtime.js';
|
|
60
|
+
export type { RuntimeOptions } from './auto/runtime.js';
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
resolveScriptPath,
|
|
64
|
+
resolveExamplePath,
|
|
65
|
+
listAutomations,
|
|
66
|
+
listExamples,
|
|
67
|
+
loadExampleConfigs,
|
|
68
|
+
ensureAutomationsDir,
|
|
69
|
+
loadAutomation,
|
|
70
|
+
} from './auto/loader.js';
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
registerAutomation,
|
|
74
|
+
unregisterAutomation,
|
|
75
|
+
cleanRegistry,
|
|
76
|
+
getAutomationsToRestart,
|
|
77
|
+
markAutomationError,
|
|
78
|
+
} from './auto/registry.js';
|
|
79
|
+
|
|
80
|
+
export type * from './auto/types.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
// Bracket Order - Entry with Take Profit and Stop Loss
|
|
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
|
|
|
@@ -43,17 +44,215 @@ Examples:
|
|
|
43
44
|
`);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
interface
|
|
47
|
+
export interface BracketOptions {
|
|
47
48
|
coin: string;
|
|
48
|
-
side: '
|
|
49
|
+
side: 'buy' | 'sell';
|
|
49
50
|
size: number;
|
|
50
|
-
entryType: 'market' | 'limit';
|
|
51
|
-
entryPrice: number;
|
|
52
|
-
tpPrice: number;
|
|
53
|
-
slPrice: number;
|
|
54
51
|
tpPct: number;
|
|
55
52
|
slPct: number;
|
|
56
|
-
|
|
53
|
+
entryType?: 'market' | 'limit';
|
|
54
|
+
entryPrice?: number;
|
|
55
|
+
slippage?: number;
|
|
56
|
+
leverage?: number;
|
|
57
|
+
dryRun?: boolean;
|
|
58
|
+
verbose?: boolean;
|
|
59
|
+
/** Receives each output line. Defaults to console.log. */
|
|
60
|
+
output?: (line: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BracketResult {
|
|
64
|
+
status: 'dry' | 'limit_resting' | 'complete' | 'entry_failed' | 'partial';
|
|
65
|
+
entryPrice?: number;
|
|
66
|
+
tpPrice?: number;
|
|
67
|
+
slPrice?: number;
|
|
68
|
+
tpOid?: number | null;
|
|
69
|
+
slOid?: number | null;
|
|
70
|
+
entryOid?: number | null;
|
|
71
|
+
reason?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runBracket(opts: BracketOptions): Promise<BracketResult> {
|
|
75
|
+
const out = opts.output ?? ((line: string) => console.log(line));
|
|
76
|
+
const entryType = opts.entryType ?? 'market';
|
|
77
|
+
const isLong = opts.side === 'buy';
|
|
78
|
+
|
|
79
|
+
if (opts.size <= 0 || isNaN(opts.size)) throw new Error('size must be positive');
|
|
80
|
+
if (opts.tpPct <= 0 || opts.slPct <= 0) throw new Error('tp and sl must be positive percentages');
|
|
81
|
+
if (entryType === 'limit' && opts.entryPrice === undefined) {
|
|
82
|
+
throw new Error('entryPrice is required for limit entry');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = getClient();
|
|
86
|
+
if (opts.verbose) client.verbose = true;
|
|
87
|
+
|
|
88
|
+
out('Open Broker - Bracket Order');
|
|
89
|
+
out('===========================\n');
|
|
90
|
+
|
|
91
|
+
const mids = await client.getAllMids();
|
|
92
|
+
const midPrice = parseFloat(mids[opts.coin]);
|
|
93
|
+
if (!midPrice) throw new Error(`No market data for ${opts.coin}`);
|
|
94
|
+
|
|
95
|
+
const entry = entryType === 'limit' ? opts.entryPrice! : midPrice;
|
|
96
|
+
|
|
97
|
+
let tpPrice = isLong
|
|
98
|
+
? entry * (1 + opts.tpPct / 100)
|
|
99
|
+
: entry * (1 - opts.tpPct / 100);
|
|
100
|
+
let slPrice = isLong
|
|
101
|
+
? entry * (1 - opts.slPct / 100)
|
|
102
|
+
: entry * (1 + opts.slPct / 100);
|
|
103
|
+
|
|
104
|
+
const riskReward = opts.tpPct / opts.slPct;
|
|
105
|
+
const notional = entry * opts.size;
|
|
106
|
+
|
|
107
|
+
out('Bracket Plan');
|
|
108
|
+
out('------------');
|
|
109
|
+
out(`Coin: ${opts.coin}`);
|
|
110
|
+
out(`Position: ${isLong ? 'LONG' : 'SHORT'}`);
|
|
111
|
+
out(`Size: ${opts.size}`);
|
|
112
|
+
out(`Entry Type: ${entryType.toUpperCase()}`);
|
|
113
|
+
out(`Current Mid: ${formatUsd(midPrice)}`);
|
|
114
|
+
out(`Entry Price: ${formatUsd(entry)}${entryType === 'market' ? ' (approx)' : ''}`);
|
|
115
|
+
out(`Take Profit: ${formatUsd(tpPrice)} (+${opts.tpPct}%)`);
|
|
116
|
+
out(`Stop Loss: ${formatUsd(slPrice)} (-${opts.slPct}%)`);
|
|
117
|
+
out(`Risk/Reward: 1:${riskReward.toFixed(2)}`);
|
|
118
|
+
out(`Est. Notional: ${formatUsd(notional)}`);
|
|
119
|
+
|
|
120
|
+
const potentialProfit = notional * (opts.tpPct / 100);
|
|
121
|
+
const potentialLoss = notional * (opts.slPct / 100);
|
|
122
|
+
out('\nRisk Analysis');
|
|
123
|
+
out('-------------');
|
|
124
|
+
out(`Potential Profit: ${formatUsd(potentialProfit)}`);
|
|
125
|
+
out(`Potential Loss: ${formatUsd(potentialLoss)}`);
|
|
126
|
+
|
|
127
|
+
if (opts.dryRun) {
|
|
128
|
+
out('\n🔍 Dry run - bracket not executed');
|
|
129
|
+
return { status: 'dry', entryPrice: entry, tpPrice, slPrice };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
out('\nExecuting bracket...\n');
|
|
133
|
+
|
|
134
|
+
// Step 1: Entry
|
|
135
|
+
out('Step 1: Entry order');
|
|
136
|
+
let actualEntry = entry;
|
|
137
|
+
let entryOid: number | null = null;
|
|
138
|
+
|
|
139
|
+
if (entryType === 'market') {
|
|
140
|
+
const entryResponse = await client.marketOrder(opts.coin, isLong, opts.size, opts.slippage, opts.leverage);
|
|
141
|
+
|
|
142
|
+
if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
|
|
143
|
+
const status = entryResponse.response.data.statuses[0];
|
|
144
|
+
if (status?.filled) {
|
|
145
|
+
actualEntry = parseFloat(status.filled.avgPx);
|
|
146
|
+
out(` ✅ Filled @ ${formatUsd(actualEntry)}`);
|
|
147
|
+
} else if (status?.error) {
|
|
148
|
+
out(` ❌ Entry failed: ${status.error}`);
|
|
149
|
+
out('\n⚠️ Bracket aborted - no position opened');
|
|
150
|
+
return { status: 'entry_failed', reason: status.error };
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const reason = typeof entryResponse.response === 'string' ? entryResponse.response : 'Unknown error';
|
|
154
|
+
out(` ❌ Entry failed: ${reason}`);
|
|
155
|
+
out('\n⚠️ Bracket aborted - no position opened');
|
|
156
|
+
return { status: 'entry_failed', reason };
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
const entryResponse = await client.limitOrder(opts.coin, isLong, opts.size, entry, 'Gtc', false, opts.leverage);
|
|
160
|
+
|
|
161
|
+
if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
|
|
162
|
+
const status = entryResponse.response.data.statuses[0];
|
|
163
|
+
if (status?.resting) {
|
|
164
|
+
entryOid = status.resting.oid;
|
|
165
|
+
out(` ✅ Limit order placed @ ${formatUsd(entry)} (OID: ${entryOid})`);
|
|
166
|
+
out(` ⏳ Waiting for fill before placing TP/SL...`);
|
|
167
|
+
out('\n⚠️ Note: TP/SL will be placed after entry fills. Monitor manually or use a strategy script.');
|
|
168
|
+
return { status: 'limit_resting', entryOid, entryPrice: entry };
|
|
169
|
+
} else if (status?.filled) {
|
|
170
|
+
actualEntry = parseFloat(status.filled.avgPx);
|
|
171
|
+
out(` ✅ Filled immediately @ ${formatUsd(actualEntry)}`);
|
|
172
|
+
} else if (status?.error) {
|
|
173
|
+
out(` ❌ Entry failed: ${status.error}`);
|
|
174
|
+
return { status: 'entry_failed', reason: status.error };
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
out(` ❌ Entry failed`);
|
|
178
|
+
return { status: 'entry_failed', reason: 'Unknown error' };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Recalculate TP/SL based on actual entry
|
|
183
|
+
if (isLong) {
|
|
184
|
+
tpPrice = actualEntry * (1 + opts.tpPct / 100);
|
|
185
|
+
slPrice = actualEntry * (1 - opts.slPct / 100);
|
|
186
|
+
} else {
|
|
187
|
+
tpPrice = actualEntry * (1 - opts.tpPct / 100);
|
|
188
|
+
slPrice = actualEntry * (1 + opts.slPct / 100);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await sleep(500);
|
|
192
|
+
|
|
193
|
+
// Step 2: Take Profit (trigger order)
|
|
194
|
+
out('\nStep 2: Take Profit order (trigger)');
|
|
195
|
+
const tpSide = !isLong;
|
|
196
|
+
const tpResponse = await client.takeProfit(opts.coin, tpSide, opts.size, tpPrice);
|
|
197
|
+
|
|
198
|
+
let tpOid: number | null = null;
|
|
199
|
+
if (tpResponse.status === 'ok' && tpResponse.response && typeof tpResponse.response === 'object') {
|
|
200
|
+
const status = tpResponse.response.data.statuses[0];
|
|
201
|
+
if (status?.resting) {
|
|
202
|
+
tpOid = status.resting.oid;
|
|
203
|
+
out(` ✅ TP trigger placed @ ${formatUsd(tpPrice)} (OID: ${tpOid})`);
|
|
204
|
+
} else if (status?.error) {
|
|
205
|
+
out(` ❌ TP failed: ${status.error}`);
|
|
206
|
+
} else {
|
|
207
|
+
out(` ⚠️ TP status: ${JSON.stringify(status)}`);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
const reason = typeof tpResponse.response === 'string' ? tpResponse.response : 'Unknown error';
|
|
211
|
+
out(` ❌ TP failed: ${reason}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await sleep(500);
|
|
215
|
+
|
|
216
|
+
// Step 3: Stop Loss (trigger order)
|
|
217
|
+
out('\nStep 3: Stop Loss order (trigger)');
|
|
218
|
+
const slSide = !isLong;
|
|
219
|
+
const slResponse = await client.stopLoss(opts.coin, slSide, opts.size, slPrice);
|
|
220
|
+
|
|
221
|
+
let slOid: number | null = null;
|
|
222
|
+
if (slResponse.status === 'ok' && slResponse.response && typeof slResponse.response === 'object') {
|
|
223
|
+
const status = slResponse.response.data.statuses[0];
|
|
224
|
+
if (status?.resting) {
|
|
225
|
+
slOid = status.resting.oid;
|
|
226
|
+
out(` ✅ SL trigger placed @ ${formatUsd(slPrice)} (OID: ${slOid})`);
|
|
227
|
+
} else if (status?.error) {
|
|
228
|
+
out(` ❌ SL failed: ${status.error}`);
|
|
229
|
+
} else {
|
|
230
|
+
out(` ⚠️ SL status: ${JSON.stringify(status)}`);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
const reason = typeof slResponse.response === 'string' ? slResponse.response : 'Unknown error';
|
|
234
|
+
out(` ❌ SL failed: ${reason}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
out('\n========== Bracket Summary ==========');
|
|
238
|
+
out(`Position: ${isLong ? 'LONG' : 'SHORT'} ${opts.size} ${opts.coin}`);
|
|
239
|
+
out(`Entry: ${formatUsd(actualEntry)}`);
|
|
240
|
+
out(`Take Profit: ${formatUsd(tpPrice)} (+${opts.tpPct}%) - Trigger order`);
|
|
241
|
+
out(`Stop Loss: ${formatUsd(slPrice)} (-${opts.slPct}%) - Trigger order`);
|
|
242
|
+
if (tpOid && slOid) {
|
|
243
|
+
out(`\n✅ Bracket complete! TP and SL are trigger orders.`);
|
|
244
|
+
out(` They will only execute when price reaches trigger level.`);
|
|
245
|
+
out(` When one fills, cancel the other manually.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
status: tpOid && slOid ? 'complete' : 'partial',
|
|
250
|
+
entryPrice: actualEntry,
|
|
251
|
+
tpPrice,
|
|
252
|
+
slPrice,
|
|
253
|
+
tpOid,
|
|
254
|
+
slOid,
|
|
255
|
+
};
|
|
57
256
|
}
|
|
58
257
|
|
|
59
258
|
async function main() {
|
|
@@ -74,214 +273,34 @@ async function main() {
|
|
|
74
273
|
printUsage();
|
|
75
274
|
process.exit(1);
|
|
76
275
|
}
|
|
77
|
-
|
|
78
276
|
if (side !== 'buy' && side !== 'sell') {
|
|
79
277
|
console.error('Error: --side must be "buy" or "sell"');
|
|
80
278
|
process.exit(1);
|
|
81
279
|
}
|
|
82
280
|
|
|
83
|
-
if (entryType === 'limit' && !entryPrice) {
|
|
84
|
-
console.error('Error: --price is required for limit entry');
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (tpPct <= 0 || slPct <= 0) {
|
|
89
|
-
console.error('Error: --tp and --sl must be positive percentages');
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const isLong = side === 'buy';
|
|
94
|
-
const client = getClient();
|
|
95
|
-
|
|
96
|
-
if (args.verbose) {
|
|
97
|
-
client.verbose = true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.log('Open Broker - Bracket Order');
|
|
101
|
-
console.log('===========================\n');
|
|
102
|
-
|
|
103
281
|
try {
|
|
104
|
-
const
|
|
105
|
-
const midPrice = parseFloat(mids[coin]);
|
|
106
|
-
if (!midPrice) {
|
|
107
|
-
console.error(`Error: No market data for ${coin}`);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Calculate prices
|
|
112
|
-
const entry = entryType === 'limit' ? entryPrice! : midPrice;
|
|
113
|
-
|
|
114
|
-
let tpPrice: number;
|
|
115
|
-
let slPrice: number;
|
|
116
|
-
|
|
117
|
-
if (isLong) {
|
|
118
|
-
// Long: TP above, SL below
|
|
119
|
-
tpPrice = entry * (1 + tpPct / 100);
|
|
120
|
-
slPrice = entry * (1 - slPct / 100);
|
|
121
|
-
} else {
|
|
122
|
-
// Short: TP below, SL above
|
|
123
|
-
tpPrice = entry * (1 - tpPct / 100);
|
|
124
|
-
slPrice = entry * (1 + slPct / 100);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const riskReward = tpPct / slPct;
|
|
128
|
-
const notional = entry * size;
|
|
129
|
-
|
|
130
|
-
const plan: BracketPlan = {
|
|
282
|
+
const result = await runBracket({
|
|
131
283
|
coin,
|
|
132
|
-
side:
|
|
284
|
+
side: side as 'buy' | 'sell',
|
|
133
285
|
size,
|
|
134
|
-
entryType,
|
|
135
|
-
entryPrice: entry,
|
|
136
|
-
tpPrice,
|
|
137
|
-
slPrice,
|
|
138
286
|
tpPct,
|
|
139
287
|
slPct,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(`Entry Type: ${entryType.toUpperCase()}`);
|
|
149
|
-
console.log(`Current Mid: ${formatUsd(midPrice)}`);
|
|
150
|
-
console.log(`Entry Price: ${formatUsd(entry)}${entryType === 'market' ? ' (approx)' : ''}`);
|
|
151
|
-
console.log(`Take Profit: ${formatUsd(tpPrice)} (+${tpPct}%)`);
|
|
152
|
-
console.log(`Stop Loss: ${formatUsd(slPrice)} (-${slPct}%)`);
|
|
153
|
-
console.log(`Risk/Reward: 1:${riskReward.toFixed(2)}`);
|
|
154
|
-
console.log(`Est. Notional: ${formatUsd(notional)}`);
|
|
155
|
-
|
|
156
|
-
// Risk analysis
|
|
157
|
-
const potentialProfit = notional * (tpPct / 100);
|
|
158
|
-
const potentialLoss = notional * (slPct / 100);
|
|
159
|
-
console.log(`\nRisk Analysis`);
|
|
160
|
-
console.log('-------------');
|
|
161
|
-
console.log(`Potential Profit: ${formatUsd(potentialProfit)}`);
|
|
162
|
-
console.log(`Potential Loss: ${formatUsd(potentialLoss)}`);
|
|
163
|
-
|
|
164
|
-
if (dryRun) {
|
|
165
|
-
console.log('\n🔍 Dry run - bracket not executed');
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
console.log('\nExecuting bracket...\n');
|
|
170
|
-
|
|
171
|
-
// Step 1: Entry
|
|
172
|
-
console.log('Step 1: Entry order');
|
|
173
|
-
let actualEntry = entry;
|
|
174
|
-
|
|
175
|
-
if (entryType === 'market') {
|
|
176
|
-
const entryResponse = await client.marketOrder(coin, isLong, size, slippage, leverage);
|
|
177
|
-
|
|
178
|
-
if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
|
|
179
|
-
const status = entryResponse.response.data.statuses[0];
|
|
180
|
-
if (status?.filled) {
|
|
181
|
-
actualEntry = parseFloat(status.filled.avgPx);
|
|
182
|
-
console.log(` ✅ Filled @ ${formatUsd(actualEntry)}`);
|
|
183
|
-
} else if (status?.error) {
|
|
184
|
-
console.log(` ❌ Entry failed: ${status.error}`);
|
|
185
|
-
console.log('\n⚠️ Bracket aborted - no position opened');
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
console.log(` ❌ Entry failed: ${typeof entryResponse.response === 'string' ? entryResponse.response : 'Unknown error'}`);
|
|
190
|
-
console.log('\n⚠️ Bracket aborted - no position opened');
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
const entryResponse = await client.limitOrder(coin, isLong, size, entry, 'Gtc', false, leverage);
|
|
195
|
-
|
|
196
|
-
if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
|
|
197
|
-
const status = entryResponse.response.data.statuses[0];
|
|
198
|
-
if (status?.resting) {
|
|
199
|
-
console.log(` ✅ Limit order placed @ ${formatUsd(entry)} (OID: ${status.resting.oid})`);
|
|
200
|
-
console.log(` ⏳ Waiting for fill before placing TP/SL...`);
|
|
201
|
-
console.log('\n⚠️ Note: TP/SL will be placed after entry fills. Monitor manually or use a strategy script.');
|
|
202
|
-
return;
|
|
203
|
-
} else if (status?.filled) {
|
|
204
|
-
actualEntry = parseFloat(status.filled.avgPx);
|
|
205
|
-
console.log(` ✅ Filled immediately @ ${formatUsd(actualEntry)}`);
|
|
206
|
-
} else if (status?.error) {
|
|
207
|
-
console.log(` ❌ Entry failed: ${status.error}`);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
console.log(` ❌ Entry failed`);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Recalculate TP/SL based on actual entry
|
|
217
|
-
if (isLong) {
|
|
218
|
-
tpPrice = actualEntry * (1 + tpPct / 100);
|
|
219
|
-
slPrice = actualEntry * (1 - slPct / 100);
|
|
220
|
-
} else {
|
|
221
|
-
tpPrice = actualEntry * (1 - tpPct / 100);
|
|
222
|
-
slPrice = actualEntry * (1 + slPct / 100);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
await sleep(500); // Brief delay
|
|
226
|
-
|
|
227
|
-
// Step 2: Take Profit (trigger order)
|
|
228
|
-
console.log('\nStep 2: Take Profit order (trigger)');
|
|
229
|
-
const tpSide = !isLong; // Opposite of entry: long -> sell TP, short -> buy TP
|
|
230
|
-
const tpResponse = await client.takeProfit(coin, tpSide, size, tpPrice);
|
|
231
|
-
|
|
232
|
-
let tpOid: number | null = null;
|
|
233
|
-
if (tpResponse.status === 'ok' && tpResponse.response && typeof tpResponse.response === 'object') {
|
|
234
|
-
const status = tpResponse.response.data.statuses[0];
|
|
235
|
-
if (status?.resting) {
|
|
236
|
-
tpOid = status.resting.oid;
|
|
237
|
-
console.log(` ✅ TP trigger placed @ ${formatUsd(tpPrice)} (OID: ${tpOid})`);
|
|
238
|
-
} else if (status?.error) {
|
|
239
|
-
console.log(` ❌ TP failed: ${status.error}`);
|
|
240
|
-
} else {
|
|
241
|
-
console.log(` ⚠️ TP status:`, JSON.stringify(status));
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
console.log(` ❌ TP failed: ${typeof tpResponse.response === 'string' ? tpResponse.response : 'Unknown error'}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
await sleep(500);
|
|
248
|
-
|
|
249
|
-
// Step 3: Stop Loss (trigger order)
|
|
250
|
-
console.log('\nStep 3: Stop Loss order (trigger)');
|
|
251
|
-
const slSide = !isLong; // Opposite of entry: long -> sell SL, short -> buy SL
|
|
252
|
-
const slResponse = await client.stopLoss(coin, slSide, size, slPrice);
|
|
253
|
-
|
|
254
|
-
let slOid: number | null = null;
|
|
255
|
-
if (slResponse.status === 'ok' && slResponse.response && typeof slResponse.response === 'object') {
|
|
256
|
-
const status = slResponse.response.data.statuses[0];
|
|
257
|
-
if (status?.resting) {
|
|
258
|
-
slOid = status.resting.oid;
|
|
259
|
-
console.log(` ✅ SL trigger placed @ ${formatUsd(slPrice)} (OID: ${slOid})`);
|
|
260
|
-
} else if (status?.error) {
|
|
261
|
-
console.log(` ❌ SL failed: ${status.error}`);
|
|
262
|
-
} else {
|
|
263
|
-
console.log(` ⚠️ SL status:`, JSON.stringify(status));
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
console.log(` ❌ SL failed: ${typeof slResponse.response === 'string' ? slResponse.response : 'Unknown error'}`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Summary
|
|
270
|
-
console.log('\n========== Bracket Summary ==========');
|
|
271
|
-
console.log(`Position: ${isLong ? 'LONG' : 'SHORT'} ${size} ${coin}`);
|
|
272
|
-
console.log(`Entry: ${formatUsd(actualEntry)}`);
|
|
273
|
-
console.log(`Take Profit: ${formatUsd(tpPrice)} (+${tpPct}%) - Trigger order`);
|
|
274
|
-
console.log(`Stop Loss: ${formatUsd(slPrice)} (-${slPct}%) - Trigger order`);
|
|
275
|
-
if (tpOid && slOid) {
|
|
276
|
-
console.log(`\n✅ Bracket complete! TP and SL are trigger orders.`);
|
|
277
|
-
console.log(` They will only execute when price reaches trigger level.`);
|
|
278
|
-
console.log(` When one fills, cancel the other manually.`);
|
|
279
|
-
}
|
|
280
|
-
|
|
288
|
+
entryType,
|
|
289
|
+
entryPrice,
|
|
290
|
+
slippage,
|
|
291
|
+
leverage,
|
|
292
|
+
dryRun,
|
|
293
|
+
verbose: args.verbose as boolean,
|
|
294
|
+
});
|
|
295
|
+
if (result.status === 'entry_failed') process.exit(1);
|
|
281
296
|
} catch (error) {
|
|
282
|
-
console.error('Error:', error);
|
|
297
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
283
298
|
process.exit(1);
|
|
284
299
|
}
|
|
285
300
|
}
|
|
286
301
|
|
|
287
|
-
|
|
302
|
+
// Only run when invoked as a script — not when imported as a module
|
|
303
|
+
// (e.g. by `openbroker-plugin` via the lib re-export of `runBracket`).
|
|
304
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
305
|
+
main();
|
|
306
|
+
}
|