openbroker 1.0.33
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 +94 -0
- package/README.md +160 -0
- package/SKILL.md +296 -0
- package/bin/cli.ts +170 -0
- package/bin/openbroker.js +24 -0
- package/config/example.env +48 -0
- package/package.json +79 -0
- package/scripts/core/client.ts +844 -0
- package/scripts/core/config.ts +92 -0
- package/scripts/core/types.ts +192 -0
- package/scripts/core/utils.ts +156 -0
- package/scripts/info/account.ts +117 -0
- package/scripts/info/all-markets.ts +223 -0
- package/scripts/info/funding.ts +133 -0
- package/scripts/info/markets.ts +151 -0
- package/scripts/info/positions.ts +88 -0
- package/scripts/info/search-markets.ts +230 -0
- package/scripts/info/spot.ts +192 -0
- package/scripts/operations/bracket.ts +285 -0
- package/scripts/operations/cancel.ts +124 -0
- package/scripts/operations/chase.ts +236 -0
- package/scripts/operations/limit-order.ts +160 -0
- package/scripts/operations/market-order.ts +167 -0
- package/scripts/operations/scale.ts +263 -0
- package/scripts/operations/set-tpsl.ts +302 -0
- package/scripts/operations/trigger-order.ts +201 -0
- package/scripts/operations/twap.ts +222 -0
- package/scripts/setup/approve-builder.ts +178 -0
- package/scripts/setup/onboard.ts +242 -0
- package/scripts/strategies/dca.ts +292 -0
- package/scripts/strategies/funding-arb.ts +319 -0
- package/scripts/strategies/grid.ts +397 -0
- package/scripts/strategies/mm-maker.ts +411 -0
- package/scripts/strategies/mm-spread.ts +402 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Scale In/Out - Place a grid of limit orders
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs, sleep } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Scale In/Out
|
|
10
|
+
==========================
|
|
11
|
+
|
|
12
|
+
Place a grid of limit orders to scale into or out of a position.
|
|
13
|
+
Orders are distributed across price levels based on the specified range and distribution.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx tsx scripts/operations/scale.ts --coin <COIN> --side <buy|sell> --size <SIZE> --levels <N> --range <PCT>
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--coin Asset to trade (e.g., ETH, BTC)
|
|
20
|
+
--side Order side: buy or sell
|
|
21
|
+
--size Total order size in base asset
|
|
22
|
+
--levels Number of price levels (orders)
|
|
23
|
+
--range Price range from current mid (e.g., 2 for ±2%)
|
|
24
|
+
--distribution Size distribution: linear, exponential, or flat (default: linear)
|
|
25
|
+
- linear: more size at better prices
|
|
26
|
+
- exponential: much more size at better prices
|
|
27
|
+
- flat: equal size at all levels
|
|
28
|
+
--reduce Reduce-only orders (for scaling out of position)
|
|
29
|
+
--tif Time in force: GTC, ALO (default: GTC)
|
|
30
|
+
--dry Dry run - show order plan without executing
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
# Scale into 1 ETH with 5 buy orders, 2% below current price
|
|
34
|
+
npx tsx scripts/operations/scale.ts --coin ETH --side buy --size 1 --levels 5 --range 2
|
|
35
|
+
|
|
36
|
+
# Scale out of 0.5 BTC with 4 sell orders, 3% above current price (reduce-only)
|
|
37
|
+
npx tsx scripts/operations/scale.ts --coin BTC --side sell --size 0.5 --levels 4 --range 3 --reduce
|
|
38
|
+
|
|
39
|
+
# Use exponential distribution for more aggressive scaling
|
|
40
|
+
npx tsx scripts/operations/scale.ts --coin ETH --side buy --size 2 --levels 8 --range 5 --distribution exponential
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface OrderLevel {
|
|
45
|
+
level: number;
|
|
46
|
+
price: number;
|
|
47
|
+
size: number;
|
|
48
|
+
distanceFromMid: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function calculateLevels(
|
|
52
|
+
midPrice: number,
|
|
53
|
+
isBuy: boolean,
|
|
54
|
+
totalSize: number,
|
|
55
|
+
numLevels: number,
|
|
56
|
+
rangePct: number,
|
|
57
|
+
distribution: 'linear' | 'exponential' | 'flat'
|
|
58
|
+
): OrderLevel[] {
|
|
59
|
+
const levels: OrderLevel[] = [];
|
|
60
|
+
|
|
61
|
+
// Calculate weights based on distribution
|
|
62
|
+
let weights: number[] = [];
|
|
63
|
+
for (let i = 0; i < numLevels; i++) {
|
|
64
|
+
switch (distribution) {
|
|
65
|
+
case 'flat':
|
|
66
|
+
weights.push(1);
|
|
67
|
+
break;
|
|
68
|
+
case 'linear':
|
|
69
|
+
weights.push(i + 1); // 1, 2, 3, 4, 5... (more at worse prices = better for buyer)
|
|
70
|
+
break;
|
|
71
|
+
case 'exponential':
|
|
72
|
+
weights.push(Math.pow(2, i)); // 1, 2, 4, 8, 16...
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
78
|
+
|
|
79
|
+
// Calculate price levels
|
|
80
|
+
for (let i = 0; i < numLevels; i++) {
|
|
81
|
+
// Distance increases with level (i=0 is closest to mid)
|
|
82
|
+
const distancePct = ((i + 1) / numLevels) * rangePct;
|
|
83
|
+
|
|
84
|
+
// Buy orders go below mid, sell orders go above
|
|
85
|
+
const price = isBuy
|
|
86
|
+
? midPrice * (1 - distancePct / 100)
|
|
87
|
+
: midPrice * (1 + distancePct / 100);
|
|
88
|
+
|
|
89
|
+
const size = (weights[i] / totalWeight) * totalSize;
|
|
90
|
+
|
|
91
|
+
levels.push({
|
|
92
|
+
level: i + 1,
|
|
93
|
+
price,
|
|
94
|
+
size,
|
|
95
|
+
distanceFromMid: distancePct,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return levels;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const args = parseArgs(process.argv.slice(2));
|
|
104
|
+
|
|
105
|
+
const coin = args.coin as string;
|
|
106
|
+
const side = args.side as string;
|
|
107
|
+
const totalSize = parseFloat(args.size as string);
|
|
108
|
+
const numLevels = parseInt(args.levels as string);
|
|
109
|
+
const rangePct = parseFloat(args.range as string);
|
|
110
|
+
const distribution = (args.distribution as string || 'linear') as 'linear' | 'exponential' | 'flat';
|
|
111
|
+
const reduceOnly = args.reduce as boolean;
|
|
112
|
+
const tifArg = ((args.tif as string)?.toUpperCase() || 'GTC');
|
|
113
|
+
const dryRun = args.dry as boolean;
|
|
114
|
+
|
|
115
|
+
if (!coin || !side || isNaN(totalSize) || isNaN(numLevels) || isNaN(rangePct)) {
|
|
116
|
+
printUsage();
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
121
|
+
console.error('Error: --side must be "buy" or "sell"');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!['linear', 'exponential', 'flat'].includes(distribution)) {
|
|
126
|
+
console.error('Error: --distribution must be linear, exponential, or flat');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Map uppercase CLI input to Pascal case for SDK
|
|
131
|
+
const tifMap: Record<string, 'Gtc' | 'Alo'> = {
|
|
132
|
+
'GTC': 'Gtc',
|
|
133
|
+
'ALO': 'Alo'
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const tif = tifMap[tifArg];
|
|
137
|
+
if (!tif) {
|
|
138
|
+
console.error('Error: --tif must be GTC or ALO');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const isBuy = side === 'buy';
|
|
143
|
+
const client = getClient();
|
|
144
|
+
|
|
145
|
+
if (args.verbose) {
|
|
146
|
+
client.verbose = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log('Open Broker - Scale In/Out');
|
|
150
|
+
console.log('==========================\n');
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const mids = await client.getAllMids();
|
|
154
|
+
const midPrice = parseFloat(mids[coin]);
|
|
155
|
+
if (!midPrice) {
|
|
156
|
+
console.error(`Error: No market data for ${coin}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const levels = calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution);
|
|
161
|
+
const notional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
|
|
162
|
+
const avgPrice = notional / totalSize;
|
|
163
|
+
|
|
164
|
+
console.log('Order Plan');
|
|
165
|
+
console.log('----------');
|
|
166
|
+
console.log(`Coin: ${coin}`);
|
|
167
|
+
console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
|
|
168
|
+
console.log(`Total Size: ${totalSize}`);
|
|
169
|
+
console.log(`Current Mid: ${formatUsd(midPrice)}`);
|
|
170
|
+
console.log(`Levels: ${numLevels}`);
|
|
171
|
+
console.log(`Range: ${rangePct}% ${isBuy ? 'below' : 'above'} mid`);
|
|
172
|
+
console.log(`Distribution: ${distribution}`);
|
|
173
|
+
console.log(`Time in Force: ${tif}`);
|
|
174
|
+
console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
|
|
175
|
+
console.log(`Est. Notional: ${formatUsd(notional)}`);
|
|
176
|
+
console.log(`Avg Price: ${formatUsd(avgPrice)}`);
|
|
177
|
+
|
|
178
|
+
console.log('\nOrder Grid');
|
|
179
|
+
console.log('----------');
|
|
180
|
+
console.log('Level | Price | Size | Distance');
|
|
181
|
+
console.log('------|--------------|------------|----------');
|
|
182
|
+
|
|
183
|
+
for (const level of levels) {
|
|
184
|
+
console.log(
|
|
185
|
+
` ${level.level.toString().padStart(2)} | ` +
|
|
186
|
+
`${formatUsd(level.price).padStart(12)} | ` +
|
|
187
|
+
`${level.size.toFixed(6).padStart(10)} | ` +
|
|
188
|
+
`${level.distanceFromMid.toFixed(2)}%`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
console.log('\n🔍 Dry run - orders not placed');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log('\nPlacing orders...\n');
|
|
198
|
+
|
|
199
|
+
const results: { level: number; oid?: number; error?: string }[] = [];
|
|
200
|
+
|
|
201
|
+
for (const level of levels) {
|
|
202
|
+
process.stdout.write(`Level ${level.level}: ${formatUsd(level.price)} x ${level.size.toFixed(6)}... `);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const response = await client.limitOrder(
|
|
206
|
+
coin,
|
|
207
|
+
isBuy,
|
|
208
|
+
level.size,
|
|
209
|
+
level.price,
|
|
210
|
+
tif,
|
|
211
|
+
reduceOnly
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
215
|
+
const status = response.response.data.statuses[0];
|
|
216
|
+
if (status?.resting) {
|
|
217
|
+
console.log(`✅ OID: ${status.resting.oid}`);
|
|
218
|
+
results.push({ level: level.level, oid: status.resting.oid });
|
|
219
|
+
} else if (status?.filled) {
|
|
220
|
+
console.log(`✅ Filled immediately @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
|
|
221
|
+
results.push({ level: level.level, oid: status.filled.oid });
|
|
222
|
+
} else if (status?.error) {
|
|
223
|
+
console.log(`❌ ${status.error}`);
|
|
224
|
+
results.push({ level: level.level, error: status.error });
|
|
225
|
+
} else {
|
|
226
|
+
console.log(`⚠️ Unknown status`);
|
|
227
|
+
results.push({ level: level.level, error: 'Unknown status' });
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
const error = typeof response.response === 'string' ? response.response : 'Failed';
|
|
231
|
+
console.log(`❌ ${error}`);
|
|
232
|
+
results.push({ level: level.level, error });
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
236
|
+
console.log(`❌ ${error}`);
|
|
237
|
+
results.push({ level: level.level, error });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Small delay between orders
|
|
241
|
+
await sleep(100);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Summary
|
|
245
|
+
const successful = results.filter(r => r.oid).length;
|
|
246
|
+
const failed = results.filter(r => r.error).length;
|
|
247
|
+
|
|
248
|
+
console.log('\n========== Summary ==========');
|
|
249
|
+
console.log(`Orders Placed: ${successful}/${numLevels}`);
|
|
250
|
+
if (failed > 0) {
|
|
251
|
+
console.log(`Failed: ${failed}`);
|
|
252
|
+
}
|
|
253
|
+
if (successful > 0) {
|
|
254
|
+
console.log(`Order IDs: ${results.filter(r => r.oid).map(r => r.oid).join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('Error:', error);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main();
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// Set Take Profit and/or Stop Loss on an existing position
|
|
3
|
+
|
|
4
|
+
import { getClient } from '../core/client.js';
|
|
5
|
+
import { formatUsd, parseArgs, sleep } from '../core/utils.js';
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Open Broker - Set TP/SL
|
|
10
|
+
=======================
|
|
11
|
+
|
|
12
|
+
Add take profit and/or stop loss orders to an existing position.
|
|
13
|
+
Uses trigger orders that execute when price reaches the target.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx tsx scripts/operations/set-tpsl.ts --coin <COIN> [--tp <PRICE>] [--sl <PRICE>]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--coin Asset with open position (e.g., ETH, BTC, HYPE)
|
|
20
|
+
--tp Take profit trigger price
|
|
21
|
+
--sl Stop loss trigger price
|
|
22
|
+
--size Size to protect (default: full position size)
|
|
23
|
+
--sl-slippage Stop loss slippage in bps (default: 100 = 1%)
|
|
24
|
+
--dry Dry run - show orders without placing
|
|
25
|
+
--verbose Show debug output
|
|
26
|
+
|
|
27
|
+
Price Formats:
|
|
28
|
+
--tp 40 Absolute price ($40)
|
|
29
|
+
--tp +10% Percentage above entry price
|
|
30
|
+
--sl -5% Percentage below entry price (for longs)
|
|
31
|
+
--sl entry Stop loss at entry price (breakeven)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
# Set TP at $40 and SL at $30 on HYPE long
|
|
35
|
+
npx tsx scripts/operations/set-tpsl.ts --coin HYPE --tp 40 --sl 30
|
|
36
|
+
|
|
37
|
+
# Set TP at +10% from entry, SL at entry (breakeven)
|
|
38
|
+
npx tsx scripts/operations/set-tpsl.ts --coin HYPE --tp +10% --sl entry
|
|
39
|
+
|
|
40
|
+
# Set only stop loss at -5% from entry
|
|
41
|
+
npx tsx scripts/operations/set-tpsl.ts --coin ETH --sl -5%
|
|
42
|
+
|
|
43
|
+
# Set TP/SL on partial position
|
|
44
|
+
npx tsx scripts/operations/set-tpsl.ts --coin ETH --tp 4000 --sl 3500 --size 0.5
|
|
45
|
+
|
|
46
|
+
How Trigger Orders Work:
|
|
47
|
+
- TP/SL are trigger orders, NOT regular limit orders
|
|
48
|
+
- They sit dormant until price reaches the trigger level
|
|
49
|
+
- Once triggered, they execute as limit orders
|
|
50
|
+
- These are reduce-only orders (close position, don't reverse)
|
|
51
|
+
- SL has slippage buffer to ensure fill in fast markets
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parsePrice(input: string, entryPrice: number, isLong: boolean): number | null {
|
|
56
|
+
if (!input) return null;
|
|
57
|
+
|
|
58
|
+
// Handle "entry" keyword for breakeven
|
|
59
|
+
if (input.toLowerCase() === 'entry') {
|
|
60
|
+
return entryPrice;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle percentage format: +10%, -5%
|
|
64
|
+
const pctMatch = input.match(/^([+-]?)(\d+(?:\.\d+)?)%$/);
|
|
65
|
+
if (pctMatch) {
|
|
66
|
+
const sign = pctMatch[1] || '+';
|
|
67
|
+
const pct = parseFloat(pctMatch[2]) / 100;
|
|
68
|
+
|
|
69
|
+
if (sign === '+') {
|
|
70
|
+
return entryPrice * (1 + pct);
|
|
71
|
+
} else {
|
|
72
|
+
return entryPrice * (1 - pct);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle absolute price
|
|
77
|
+
const price = parseFloat(input);
|
|
78
|
+
if (!isNaN(price) && price > 0) {
|
|
79
|
+
return price;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
const args = parseArgs(process.argv.slice(2));
|
|
87
|
+
|
|
88
|
+
if (args.help) {
|
|
89
|
+
printUsage();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const coin = args.coin as string;
|
|
94
|
+
const tpInput = args.tp as string | undefined;
|
|
95
|
+
const slInput = args.sl as string | undefined;
|
|
96
|
+
const sizeOverride = args.size ? parseFloat(args.size as string) : undefined;
|
|
97
|
+
const slSlippage = args['sl-slippage'] ? parseInt(args['sl-slippage'] as string) : 100;
|
|
98
|
+
const dryRun = args.dry as boolean;
|
|
99
|
+
|
|
100
|
+
if (!coin) {
|
|
101
|
+
printUsage();
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!tpInput && !slInput) {
|
|
106
|
+
console.error('Error: Must specify at least --tp or --sl');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const client = getClient();
|
|
111
|
+
|
|
112
|
+
if (args.verbose) {
|
|
113
|
+
client.verbose = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log('Open Broker - Set TP/SL');
|
|
117
|
+
console.log('=======================\n');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Get current position
|
|
121
|
+
const userState = await client.getUserState();
|
|
122
|
+
const position = userState.assetPositions.find(p => p.position.coin === coin);
|
|
123
|
+
|
|
124
|
+
if (!position) {
|
|
125
|
+
console.error(`Error: No open position for ${coin}`);
|
|
126
|
+
console.log('\nYour positions:');
|
|
127
|
+
for (const pos of userState.assetPositions) {
|
|
128
|
+
const size = parseFloat(pos.position.szi);
|
|
129
|
+
if (Math.abs(size) > 0) {
|
|
130
|
+
console.log(` ${pos.position.coin}: ${size > 0 ? 'LONG' : 'SHORT'} ${Math.abs(size)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const posSize = parseFloat(position.position.szi);
|
|
137
|
+
const entryPrice = parseFloat(position.position.entryPx);
|
|
138
|
+
const isLong = posSize > 0;
|
|
139
|
+
const absSize = Math.abs(posSize);
|
|
140
|
+
const size = sizeOverride ?? absSize;
|
|
141
|
+
|
|
142
|
+
// Get current price
|
|
143
|
+
const mids = await client.getAllMids();
|
|
144
|
+
const currentPrice = parseFloat(mids[coin]);
|
|
145
|
+
|
|
146
|
+
// Parse TP and SL prices
|
|
147
|
+
const tpPrice = tpInput ? parsePrice(tpInput, entryPrice, isLong) : null;
|
|
148
|
+
const slPrice = slInput ? parsePrice(slInput, entryPrice, isLong) : null;
|
|
149
|
+
|
|
150
|
+
if (tpInput && tpPrice === null) {
|
|
151
|
+
console.error(`Error: Invalid TP price format: ${tpInput}`);
|
|
152
|
+
console.log('Use absolute price (e.g., 40), percentage (e.g., +10%), or "entry"');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (slInput && slPrice === null) {
|
|
157
|
+
console.error(`Error: Invalid SL price format: ${slInput}`);
|
|
158
|
+
console.log('Use absolute price (e.g., 35), percentage (e.g., -5%), or "entry"');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate TP/SL make sense for position direction
|
|
163
|
+
if (isLong) {
|
|
164
|
+
if (tpPrice && tpPrice <= currentPrice) {
|
|
165
|
+
console.warn(`⚠️ Warning: TP (${formatUsd(tpPrice)}) is at or below current price (${formatUsd(currentPrice)})`);
|
|
166
|
+
console.warn(' For LONG positions, TP should be above current price');
|
|
167
|
+
}
|
|
168
|
+
if (slPrice && slPrice >= currentPrice) {
|
|
169
|
+
console.warn(`⚠️ Warning: SL (${formatUsd(slPrice)}) is at or above current price (${formatUsd(currentPrice)})`);
|
|
170
|
+
console.warn(' For LONG positions, SL should be below current price');
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
if (tpPrice && tpPrice >= currentPrice) {
|
|
174
|
+
console.warn(`⚠️ Warning: TP (${formatUsd(tpPrice)}) is at or above current price (${formatUsd(currentPrice)})`);
|
|
175
|
+
console.warn(' For SHORT positions, TP should be below current price');
|
|
176
|
+
}
|
|
177
|
+
if (slPrice && slPrice <= currentPrice) {
|
|
178
|
+
console.warn(`⚠️ Warning: SL (${formatUsd(slPrice)}) is at or below current price (${formatUsd(currentPrice)})`);
|
|
179
|
+
console.warn(' For SHORT positions, SL should be above current price');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Calculate risk/reward
|
|
184
|
+
let tpDistance = 0, slDistance = 0, riskReward = 0;
|
|
185
|
+
if (tpPrice) {
|
|
186
|
+
tpDistance = isLong
|
|
187
|
+
? (tpPrice - entryPrice) / entryPrice * 100
|
|
188
|
+
: (entryPrice - tpPrice) / entryPrice * 100;
|
|
189
|
+
}
|
|
190
|
+
if (slPrice) {
|
|
191
|
+
slDistance = isLong
|
|
192
|
+
? (entryPrice - slPrice) / entryPrice * 100
|
|
193
|
+
: (slPrice - entryPrice) / entryPrice * 100;
|
|
194
|
+
}
|
|
195
|
+
if (tpDistance > 0 && slDistance > 0) {
|
|
196
|
+
riskReward = tpDistance / slDistance;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('Current Position');
|
|
200
|
+
console.log('----------------');
|
|
201
|
+
console.log(`Coin: ${coin}`);
|
|
202
|
+
console.log(`Direction: ${isLong ? 'LONG' : 'SHORT'}`);
|
|
203
|
+
console.log(`Size: ${absSize}`);
|
|
204
|
+
console.log(`Entry Price: ${formatUsd(entryPrice)}`);
|
|
205
|
+
console.log(`Current Price: ${formatUsd(currentPrice)}`);
|
|
206
|
+
console.log(`Unrealized: ${formatUsd(parseFloat(position.position.unrealizedPnl))}`);
|
|
207
|
+
|
|
208
|
+
console.log('\nOrders to Place');
|
|
209
|
+
console.log('---------------');
|
|
210
|
+
if (tpPrice) {
|
|
211
|
+
const tpSide = isLong ? 'SELL' : 'BUY';
|
|
212
|
+
console.log(`Take Profit: ${tpSide} ${size} @ ${formatUsd(tpPrice)} (+${tpDistance.toFixed(2)}% from entry)`);
|
|
213
|
+
}
|
|
214
|
+
if (slPrice) {
|
|
215
|
+
const slSide = isLong ? 'SELL' : 'BUY';
|
|
216
|
+
const slLimitPrice = isLong
|
|
217
|
+
? slPrice * (1 - slSlippage / 10000)
|
|
218
|
+
: slPrice * (1 + slSlippage / 10000);
|
|
219
|
+
console.log(`Stop Loss: ${slSide} ${size} @ ${formatUsd(slPrice)} trigger, ${formatUsd(slLimitPrice)} limit (-${slDistance.toFixed(2)}%)`);
|
|
220
|
+
}
|
|
221
|
+
if (riskReward > 0) {
|
|
222
|
+
console.log(`Risk/Reward: 1:${riskReward.toFixed(2)}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Potential outcomes
|
|
226
|
+
const potentialProfit = tpPrice ? Math.abs(tpPrice - entryPrice) * size : 0;
|
|
227
|
+
const potentialLoss = slPrice ? Math.abs(entryPrice - slPrice) * size : 0;
|
|
228
|
+
console.log('\nPotential Outcomes');
|
|
229
|
+
console.log('------------------');
|
|
230
|
+
if (tpPrice) console.log(`If TP hits: +${formatUsd(potentialProfit)}`);
|
|
231
|
+
if (slPrice) console.log(`If SL hits: -${formatUsd(potentialLoss)}`);
|
|
232
|
+
|
|
233
|
+
if (dryRun) {
|
|
234
|
+
console.log('\n🔍 Dry run - orders not placed');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log('\nPlacing trigger orders...\n');
|
|
239
|
+
|
|
240
|
+
// Place Take Profit
|
|
241
|
+
let tpOid: number | null = null;
|
|
242
|
+
if (tpPrice) {
|
|
243
|
+
const tpSide = !isLong; // Opposite of position direction
|
|
244
|
+
const response = await client.takeProfit(coin, tpSide, size, tpPrice);
|
|
245
|
+
|
|
246
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
247
|
+
const status = response.response.data.statuses[0];
|
|
248
|
+
if (status?.resting) {
|
|
249
|
+
tpOid = status.resting.oid;
|
|
250
|
+
console.log(`✅ Take Profit placed @ ${formatUsd(tpPrice)} (OID: ${tpOid})`);
|
|
251
|
+
} else if (status?.error) {
|
|
252
|
+
console.log(`❌ TP failed: ${status.error}`);
|
|
253
|
+
} else {
|
|
254
|
+
console.log(`⚠️ TP status:`, JSON.stringify(status));
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
console.log(`❌ TP failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await sleep(200);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Place Stop Loss
|
|
264
|
+
let slOid: number | null = null;
|
|
265
|
+
if (slPrice) {
|
|
266
|
+
const slSide = !isLong; // Opposite of position direction
|
|
267
|
+
const response = await client.stopLoss(coin, slSide, size, slPrice, slSlippage);
|
|
268
|
+
|
|
269
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
270
|
+
const status = response.response.data.statuses[0];
|
|
271
|
+
if (status?.resting) {
|
|
272
|
+
slOid = status.resting.oid;
|
|
273
|
+
console.log(`✅ Stop Loss placed @ ${formatUsd(slPrice)} (OID: ${slOid})`);
|
|
274
|
+
} else if (status?.error) {
|
|
275
|
+
console.log(`❌ SL failed: ${status.error}`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log(`⚠️ SL status:`, JSON.stringify(status));
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
console.log(`❌ SL failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Summary
|
|
285
|
+
console.log('\n========== Summary ==========');
|
|
286
|
+
console.log(`Position: ${isLong ? 'LONG' : 'SHORT'} ${absSize} ${coin}`);
|
|
287
|
+
console.log(`Entry: ${formatUsd(entryPrice)}`);
|
|
288
|
+
if (tpOid) console.log(`Take Profit: ${formatUsd(tpPrice!)} (OID: ${tpOid})`);
|
|
289
|
+
if (slOid) console.log(`Stop Loss: ${formatUsd(slPrice!)} (OID: ${slOid})`);
|
|
290
|
+
|
|
291
|
+
if (tpOid && slOid) {
|
|
292
|
+
console.log(`\n💡 Tip: When one order fills, cancel the other manually:`);
|
|
293
|
+
console.log(` npx tsx scripts/operations/cancel.ts --coin ${coin} --oid <OID>`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('Error:', error);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main();
|