test-order.js•10.2 kB
#!/usr/bin/env node
// Simple Node.js test script to test the $80 ETH order with exact Cloud Functions signature logic
import crypto from 'crypto';
import fetch from 'node-fetch';
import dotenv from 'dotenv';
dotenv.config();
const config = {
accessKey: process.env.ACCESS_KEY || '',
secretKey: process.env.SECRET_KEY || '',
demo: process.env.DEMO?.toLowerCase() === 'true',
testnet: process.env.TESTNET?.toLowerCase() === 'true'
};
// Determine base URL
let baseUrl;
if (config.demo) {
baseUrl = 'https://api-demo.bybit.com';
console.log('Using Demo environment: https://api-demo.bybit.com');
} else if (config.testnet) {
baseUrl = 'https://api-testnet.bybit.com';
console.log('Using Testnet environment: https://api-testnet.bybit.com');
} else {
baseUrl = 'https://api.bybit.com';
console.log('Using Production environment: https://api.bybit.com');
}
async function makeBybitRequest(endpoint, method = 'GET', params = {}) {
try {
const timestamp = Date.now().toString();
// Create signature - EXACT same logic as your Cloud Functions
let queryString = '';
if (method === 'GET') {
const sortedParams = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&');
queryString = sortedParams;
} else {
// For POST requests - exact Node.js JSON.stringify behavior
queryString = params && Object.keys(params).length > 0
? JSON.stringify(params)
: '';
}
// Signature payload - exact same format as your Cloud Functions
const signaturePayload = timestamp + config.accessKey + queryString;
const signature = crypto
.createHmac('sha256', config.secretKey)
.update(signaturePayload)
.digest('hex');
const headers = {
'X-BAPI-API-KEY': config.accessKey,
'X-BAPI-SIGN': signature,
'X-BAPI-TIMESTAMP': timestamp,
'Content-Type': 'application/json',
'User-Agent': 'BybitMCP-Node-Test/1.0'
};
const url = method === 'GET' && queryString
? `${baseUrl}${endpoint}?${queryString}`
: `${baseUrl}${endpoint}`;
const requestOptions = {
method,
headers,
timeout: 10000
};
if (method === 'POST' && params && Object.keys(params).length > 0) {
requestOptions.body = JSON.stringify(params);
}
console.log(`Making ${method} request to: ${url}`);
if (method === 'POST') {
console.log('Request body:', JSON.stringify(params, null, 2));
console.log('Signature payload:', signaturePayload);
console.log('Signature:', signature);
}
const response = await fetch(url, requestOptions);
const data = await response.json();
return data;
} catch (error) {
console.error('Request failed:', error.message);
return { error: error.message };
}
}
async function testTradeExecution() {
console.log('🚀 Node.js Bybit Trade Execution Test');
console.log('=' * 45);
try {
// 1. Get current ETHUSDT price
console.log('\n1. Getting ETHUSDT price...');
const ticker = await makeBybitRequest('/v5/market/tickers', 'GET', {
category: 'linear',
symbol: 'ETHUSDT'
});
if (ticker.error || ticker.retCode !== 0) {
console.log('❌ Price error:', ticker.error || ticker.retMsg);
return;
}
const price = parseFloat(ticker.result.list[0].lastPrice);
console.log(` Current price: $${price}`);
// 2. Check wallet balance
console.log('\n2. Checking balance...');
const balance = await makeBybitRequest('/v5/account/wallet-balance', 'GET', {
accountType: 'UNIFIED'
});
if (balance.error || balance.retCode !== 0) {
console.log('❌ Balance error:', balance.error || balance.retMsg);
return;
}
const available = parseFloat(balance.result.list[0].totalAvailableBalance);
console.log(` Available: ${available} USDT`);
// 3. Get instrument info to validate position sizes
console.log('\n3. Getting ETHUSDT trading rules...');
const instrumentInfo = await makeBybitRequest('/v5/market/instruments-info', 'GET', {
category: 'linear',
symbol: 'ETHUSDT'
});
if (instrumentInfo.error || instrumentInfo.retCode !== 0) {
console.log('❌ Instrument info error:', instrumentInfo.error || instrumentInfo.retMsg);
return;
}
const instrument = instrumentInfo.result.list[0];
const lotSizeFilter = instrument.lotSizeFilter;
const minOrderQty = parseFloat(lotSizeFilter.minOrderQty);
const maxOrderQty = parseFloat(lotSizeFilter.maxOrderQty);
const qtyStep = parseFloat(lotSizeFilter.qtyStep);
console.log(` Min order quantity: ${minOrderQty} ETH`);
console.log(` Max order quantity: ${maxOrderQty} ETH`);
console.log(` Quantity step: ${qtyStep} ETH`);
// 4. Calculate order for $80 with proper validation
const targetAmount = 80.0;
let rawOrderQty = targetAmount / price;
// Round to the nearest valid quantity step
let orderQty = Math.round(rawOrderQty / qtyStep) * qtyStep;
// Ensure it meets minimum requirements
if (orderQty < minOrderQty) {
orderQty = minOrderQty;
console.log(` ⚠️ Adjusted to minimum quantity: ${orderQty} ETH`);
}
// Ensure it doesn't exceed maximum
if (orderQty > maxOrderQty) {
orderQty = maxOrderQty;
console.log(` ⚠️ Adjusted to maximum quantity: ${orderQty} ETH`);
}
// Format to the correct decimal places based on qtyStep
const decimalPlaces = qtyStep.toString().split('.')[1]?.length || 0;
const formattedQty = orderQty.toFixed(decimalPlaces);
const estimatedCost = (orderQty * price).toFixed(2);
console.log('\n4. Validated order calculation:');
console.log(` Target amount: $${targetAmount}`);
console.log(` Raw quantity: ${rawOrderQty.toFixed(6)} ETH`);
console.log(` Validated quantity: ${formattedQty} ETH`);
console.log(` Estimated cost: $${estimatedCost}`);
if (parseFloat(estimatedCost) > available) {
console.log(`❌ Insufficient balance! Need $${estimatedCost} but only have $${available}`);
return;
}
// 5. Check current positions to determine position mode
console.log('\n5. Checking current position mode...');
const currentPositions = await makeBybitRequest('/v5/position/list', 'GET', {
category: 'linear',
symbol: 'ETHUSDT'
});
let positionIdx = '0'; // Default to one-way mode
if (currentPositions.retCode === 0 && currentPositions.result.list.length > 0) {
// Check if we have separate long/short positions (hedge mode)
const positions = currentPositions.result.list;
const hasLongPosition = positions.some(p => p.side === 'Buy');
const hasShortPosition = positions.some(p => p.side === 'Sell');
if (hasLongPosition || hasShortPosition) {
positionIdx = '1'; // Long position in hedge mode
console.log(' Position mode: Hedge mode detected');
} else {
console.log(' Position mode: One-way mode (default)');
}
} else {
console.log(' Position mode: One-way mode (no existing positions)');
}
// 6. Place the order
console.log('\n6. Placing ETHUSDT long order...');
console.log(` Symbol: ETHUSDT`);
console.log(` Side: Buy (Long)`);
console.log(` Type: Market`);
console.log(` Quantity: ${formattedQty} ETH`);
console.log(` Position Index: ${positionIdx} (${positionIdx === '0' ? 'One-way' : 'Long'})`);
const orderParams = {
category: 'linear',
symbol: 'ETHUSDT',
side: 'Buy',
orderType: 'Market',
qty: formattedQty, // Use the formatted string quantity
timeInForce: 'IOC',
orderFilter: 'Order',
isLeverage: 0,
positionIdx: positionIdx
};
const order = await makeBybitRequest('/v5/order/create', 'POST', orderParams);
if (order.error || order.retCode !== 0) {
console.log(`❌ Order error: ${order.error || order.retMsg}`);
// Check specific error types
const errorMsg = (order.error || order.retMsg || '').toLowerCase();
if (errorMsg.includes('sign')) {
console.log('💡 Signature error - comparing with Python implementation');
} else if (errorMsg.includes('insufficient') || errorMsg.includes('balance')) {
console.log('💡 Insufficient balance - expected in demo mode');
} else if (errorMsg.includes('minimum') || errorMsg.includes('qty')) {
console.log('💡 Quantity issue - let\'s try adjusting the amount');
}
} else {
const orderId = order.result.orderId;
console.log(`✅ Order placed successfully!`);
console.log(` Order ID: ${orderId}`);
console.log(` Quantity: ${orderQty} ETH`);
console.log(` Estimated value: $${estimatedCost}`);
// 5. Check position after a moment
console.log('\n5. Checking position (waiting 3 seconds)...');
await new Promise(resolve => setTimeout(resolve, 3000));
const positions = await makeBybitRequest('/v5/position/list', 'GET', {
category: 'linear',
symbol: 'ETHUSDT'
});
if (positions.retCode === 0) {
const ethPos = positions.result.list.find(p =>
p.symbol === 'ETHUSDT' && parseFloat(p.size) > 0
);
if (ethPos) {
console.log(`✅ Position opened successfully!`);
console.log(` Size: ${ethPos.size} ETH`);
console.log(` Entry price: $${ethPos.avgPrice}`);
console.log(` Unrealized PnL: $${ethPos.unrealisedPnl}`);
console.log(` Position value: $${(parseFloat(ethPos.size) * parseFloat(ethPos.avgPrice)).toFixed(2)}`);
} else {
console.log('ℹ️ No position found yet (order may still be processing)');
}
} else {
console.log(`⚠️ Could not check positions: ${positions.retMsg}`);
}
}
} catch (error) {
console.error('Test failed:', error);
}
console.log('\n🎉 Test completed!');
}
// Run the test
testTradeExecution().catch(console.error);