// ============================================================ // BUILD ORDER REQUEST — HMAC-SHA256 signed Binance order // Pure-JS HMAC (crypto module blocked by n8n task-runner sandbox) // ============================================================ const API_KEY = 'kFYcAx3hJjlAtxbtSjl0DuY1s6zIXLm0YNnwhEqueWMkHvOEAkyBfAAGGfJTfoJS'; const SECRET_KEY = 'x7Em12CzxC7bTWmCUah7W6Q8DAFiZgXkmzRRLJnxnaJyAdmQtcCkU4Spun7QjYMt'; const ORDER_PATH = '/api/v3/order'; // LIVE — real orders function sha256(msgBytes) { const K = [0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2]; let h = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]; const ml = msgBytes.length * 8; msgBytes.push(0x80); while (msgBytes.length % 64 !== 56) msgBytes.push(0); for (let i = 56; i >= 0; i -= 8) msgBytes.push((ml / Math.pow(2, i)) & 0xff); for (let i = 0; i < msgBytes.length; i += 64) { const w = []; for (let j = 0; j < 16; j++) w[j] = (msgBytes[i+j*4]<<24)|(msgBytes[i+j*4+1]<<16)|(msgBytes[i+j*4+2]<<8)|msgBytes[i+j*4+3]; for (let j = 16; j < 64; j++) { const s0 = (w[j-15]>>>7|w[j-15]<<25)^(w[j-15]>>>18|w[j-15]<<14)^(w[j-15]>>>3); const s1 = (w[j-2]>>>17|w[j-2]<<15)^(w[j-2]>>>19|w[j-2]<<13)^(w[j-2]>>>10); w[j] = (w[j-16]+s0+w[j-7]+s1) >>> 0; } let [a,b,c,d,e,f,g,hh] = h; for (let j = 0; j < 64; j++) { const S1 = (e>>>6|e<<26)^(e>>>11|e<<21)^(e>>>25|e<<7); const ch = (e&f)^(~e&g); const t1 = (hh+S1+ch+K[j]+w[j]) >>> 0; const S0 = (a>>>2|a<<30)^(a>>>13|a<<19)^(a>>>22|a<<10); const maj = (a&b)^(a&c)^(b&c); const t2 = (S0+maj) >>> 0; hh=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0; } h = h.map((v,i)=>[a,b,c,d,e,f,g,hh][i]+v>>>0); } return h.map(v=>v.toString(16).padStart(8,'0')).join(''); } function strToBytes(s) { return Array.from(s).map(c => c.charCodeAt(0) & 0xff); } function hmacSha256(key, msg) { let k = strToBytes(key); const blockSize = 64; if (k.length > blockSize) { const h = sha256([...k]); k = h.match(/../g).map(x => parseInt(x,16)); } while (k.length < blockSize) k.push(0); const ipad = k.map(b => b ^ 0x36); const opad = k.map(b => b ^ 0x5c); const innerBytes = sha256([...ipad, ...strToBytes(msg)]).match(/../g).map(x => parseInt(x,16)); return sha256([...opad, ...innerBytes]); } function roundStep(qty, decimals) { const factor = Math.pow(10, decimals); return Math.floor(qty * factor) / factor; } // Decision comes from Sign Balance URL node (passed through from Decision Engine) const signNode = $('Sign Balance URL').first().json; const decision = signNode.decision || {}; const action = signNode.action || decision.action; const trade = { ...(signNode.trade || decision.trade || {}) }; // copy so we can mutate // Helper: always return a valid output with full tradeContext so Process Order Fill can read symbol etc function makeOutput(url, side, sym, qParam, qValue) { return [{ json: { url, headers: { 'X-MBX-APIKEY': API_KEY }, orderSide: side, symbol: sym, quantityParam: qParam, quantityValue: qValue, action, tradeContext: trade, btcSignal: decision.btcSignal || null, portfolio: decision.portfolio || {}, scanSummary: decision.scanSummary || [] } }]; } let side, symbol, quantityParam, quantityValue; if (action === 'buy') { const sd = $getWorkflowStaticData('global'); // Use real USDT balance from Fetch Real Balance — prevents oversized orders let realUSDT = 0; let balNode = null; try { balNode = $('Fetch Real Balance').first().json; const balances = (balNode.balances) || []; const usdtBal = balances.find(b => b.asset === 'USDT'); realUSDT = usdtBal ? parseFloat(usdtBal.free || 0) : 0; } catch(e) { realUSDT = 0; } // If real USDT < 10, we may already be in a coin position (state lost but coins held on Binance) if (realUSDT < 10) { const allBalances = (balNode && balNode.balances) || []; const STABLES = new Set(['USDT','USDC','BUSD','TUSD','DAI','FDUSD','USDP','BNB']); const heldCoin = allBalances.find(b => !STABLES.has(b.asset) && parseFloat(b.free || 0) > 0); if (heldCoin) { // We're in a position — recover sd.position from real balance so TP/SL can manage it const coinSymbol = heldCoin.asset + 'USDT'; const coinQty = parseFloat(heldCoin.free); let coinPrice = 0; try { const allItems = $('Fetch All Tickers').all(); const tickers = allItems.length > 1 ? allItems.map(i => i.json) : (() => { const r = allItems[0].json; return Array.isArray(r) ? r : Object.values(r); })(); const tk = tickers.find(t => t.symbol === coinSymbol); coinPrice = tk ? parseFloat(tk.lastPrice || 0) : 0; } catch(e) { coinPrice = 0; } if (!sd.position && coinPrice > 0 && coinQty * coinPrice > 5) { sd.position = { symbol: coinSymbol, entry: coinPrice, qty: coinQty, tradeValue: +(coinQty * coinPrice).toFixed(2), tpPct: 0.015, slPct: 0.01, highWater: coinPrice, entryTime: new Date().toISOString(), entryScore: 0 }; } sd.pendingFill = null; return makeOutput('https://api.binance.com/api/v3/ping', 'BUY', 'SKIP', 'quoteOrderQty', '0'); } // Genuinely no funds — alert via Telegram sd.pendingFill = null; const balDiag = 'realUSDT=' + realUSDT.toFixed(2) + ' | raw:' + JSON.stringify(balNode || {}).slice(0, 250); const out = makeOutput('https://api.binance.com/api/v3/ping', 'BUY', trade.symbol || 'SKIP', 'quoteOrderQty', '0'); out[0].json.balanceFetchError = true; out[0].json.balanceFetchDiag = balDiag; return out; } // Use 98% of real free USDT, also sync sd.capital sd.capital = realUSDT; const tradeValue = +(realUSDT * 0.98).toFixed(2); side = 'BUY'; symbol = trade.symbol; quantityParam = 'quoteOrderQty'; quantityValue = tradeValue.toFixed(2); trade.tradeValue = tradeValue; trade.realBalance = realUSDT; } else if (action === 'close_tp' || action === 'close_sl' || action === 'close_switch') { const sd = $getWorkflowStaticData('global'); symbol = trade.symbol || (sd.position && sd.position.symbol); // Use real coin balance from Fetch Real Balance — avoids -2010 from qty mismatch let realCoinQty = 0; try { const balNode = $('Fetch Real Balance').first().json; const balances = balNode.balances || []; const base = symbol.replace('USDT', ''); const coinBal = balances.find(b => b.asset === base); realCoinQty = coinBal ? parseFloat(coinBal.free || 0) : 0; } catch(e) { realCoinQty = 0; } // Fall back to sd.position.qty if balance fetch failed if (realCoinQty <= 0) realCoinQty = sd.position ? sd.position.qty : (trade.qty || 0); // Get LOT_SIZE stepDecimals from Fetch Exchange Info node let stepDp = 2; // safe default try { const exInfo = $('Fetch Exchange Info').first().json; const lot = ((exInfo.symbols || [])[0]?.filters || []).find(f => f.filterType === 'LOT_SIZE'); if (lot) { const step = lot.stepSize.replace(/0+$/, ''); stepDp = step.includes('.') ? step.split('.')[1].length : 0; } } catch(e) { stepDp = 2; } const factor = Math.pow(10, stepDp); side = 'SELL'; quantityParam = 'quantity'; quantityValue = Math.floor(realCoinQty * factor) / factor + ''; } else { // Unknown action — release guard and pass through const sd = $getWorkflowStaticData('global'); sd.pendingFill = null; return makeOutput('https://api.binance.com/api/v3/ping', 'BUY', trade.symbol || 'SKIP', 'quoteOrderQty', '0'); } const timestamp = Date.now(); const queryString = 'symbol=' + symbol + '&side=' + side + '&type=MARKET&' + quantityParam + '=' + quantityValue + '&recvWindow=5000×tamp=' + timestamp; const signature = hmacSha256(SECRET_KEY, queryString); const signedUrl = 'https://api.binance.com' + ORDER_PATH + '?' + queryString + '&signature=' + signature; return makeOutput(signedUrl, side, symbol, quantityParam, quantityValue);