← Back
'use strict';

const crypto = require('crypto');
const axios = require('axios');
const config = require('../config');
const logger = require('../utils/logger');

const BASE = config.binance.eapiBase;
const API_KEY = config.binance.apiKey;
const API_SECRET = config.binance.apiSecret;

// ─── Signature helper ────────────────────────────────────

function sign(params) {
  const qs = new URLSearchParams(params).toString();
  const signature = crypto.createHmac('sha256', API_SECRET).update(qs).digest('hex');
  return `${qs}&signature=${signature}`;
}

function headers() {
  return { 'X-MBX-APIKEY': API_KEY };
}

function ensureEnabled() {
  if (!config.trading.enabled) {
    throw new Error('Trading is disabled. Set TRADING_ENABLED=true in .env');
  }
  if (!API_KEY || !API_SECRET) {
    throw new Error('Binance API credentials not configured');
  }
}

// ─── Account Info ────────────────────────────────────────

async function getAccount() {
  ensureEnabled();
  const params = { timestamp: Date.now(), recvWindow: 5000 };
  const resp = await axios.get(`${BASE}/eapi/v1/marginAccount?${sign(params)}`, { headers: headers() });
  return resp.data;
}

// ─── Open Positions ──────────────────────────────────────

async function getPositions() {
  ensureEnabled();
  const params = { timestamp: Date.now(), recvWindow: 5000 };
  const resp = await axios.get(`${BASE}/eapi/v1/position?${sign(params)}`, { headers: headers() });
  return resp.data;
}

// ─── Place Order ─────────────────────────────────────────
// symbol: "BTC-260424-95000-C"
// side: "BUY" | "SELL"
// type: "LIMIT" | "MARKET"
// quantity: number (contracts)
// price: number (premium, required for LIMIT)

async function placeOrder({ symbol, side, quantity, price, reduceOnly = false, timeInForce }) {
  ensureEnabled();

  // Binance eAPI only supports LIMIT orders for options
  if (!price) throw new Error('Price required — Binance options only support LIMIT orders');

  // Validate qty: min 0.01, step 0.01
  const qty = Math.round(parseFloat(quantity) * 100) / 100; // round to 2 decimals
  if (qty < 0.01) throw new Error('Min quantity is 0.01');

  const params = {
    symbol,
    side: side.toUpperCase(),
    type: 'LIMIT',
    quantity: String(qty),
    price: String(price),
    timeInForce: timeInForce || 'GTC',
    timestamp: Date.now(),
    recvWindow: 5000,
  };

  if (reduceOnly) params.reduceOnly = 'true';

  logger.info(`[TRADE] Placing ${side} LIMIT ${qty} × ${symbol} @ $${price}`);

  const resp = await axios.post(`${BASE}/eapi/v1/order?${sign(params)}`, null, { headers: headers() });

  logger.info(`[TRADE] Order placed: orderId=${resp.data.orderId} status=${resp.data.status}`);
  return resp.data;
}

// ─── Cancel Order ────────────────────────────────────────

async function cancelOrder(symbol, orderId) {
  ensureEnabled();
  const params = { symbol, orderId: String(orderId), timestamp: Date.now(), recvWindow: 5000 };
  const resp = await axios.delete(`${BASE}/eapi/v1/order?${sign(params)}`, { headers: headers() });
  logger.info(`[TRADE] Order cancelled: ${orderId}`);
  return resp.data;
}

// ─── Cancel All Orders for Symbol ────────────────────────

async function cancelAllOrders(symbol) {
  ensureEnabled();
  const params = { symbol, timestamp: Date.now(), recvWindow: 5000 };
  const resp = await axios.delete(`${BASE}/eapi/v1/allOpenOrders?${sign(params)}`, { headers: headers() });
  return resp.data;
}

// ─── Open Orders ─────────────────────────────────────────

async function getOpenOrders(symbol) {
  ensureEnabled();
  const params = { timestamp: Date.now(), recvWindow: 5000 };
  if (symbol) params.symbol = symbol;
  const resp = await axios.get(`${BASE}/eapi/v1/openOrders?${sign(params)}`, { headers: headers() });
  return resp.data;
}

// ─── Trade History ───────────────────────────────────────

async function getTradeHistory(symbol, limit = 20) {
  ensureEnabled();
  const params = { symbol, limit, timestamp: Date.now(), recvWindow: 5000 };
  const resp = await axios.get(`${BASE}/eapi/v1/userTrades?${sign(params)}`, { headers: headers() });
  return resp.data;
}

// ─── Exercise History ────────────────────────────────────
// Returns expired/exercised options. strikeResult:
//   EXTRINSIC_VALUE_EXPIRED = expired worthless (OTM)
//   REALISTIC_VALUE_STRICKEN = auto-exercised (ITM at expiry)

async function getExerciseHistory({ symbol, startTime, endTime, limit = 100 } = {}) {
  ensureEnabled();
  const params = { timestamp: Date.now(), recvWindow: 5000, limit };
  if (symbol) params.symbol = symbol;
  if (startTime) params.startTime = startTime;
  if (endTime) params.endTime = endTime;
  const resp = await axios.get(`${BASE}/eapi/v1/exerciseHistory?${sign(params)}`, { headers: headers() });
  return resp.data;
}

// ─── Order Query ─────────────────────────────────────────

async function queryOrder(symbol, orderId) {
  ensureEnabled();
  const params = { symbol, orderId: String(orderId), timestamp: Date.now(), recvWindow: 5000 };
  const resp = await axios.get(`${BASE}/eapi/v1/order?${sign(params)}`, { headers: headers() });
  return resp.data;
}

module.exports = {
  getAccount,
  getPositions,
  placeOrder,
  cancelOrder,
  cancelAllOrders,
  getOpenOrders,
  getTradeHistory,
  getExerciseHistory,
  queryOrder,
};

📜 Git History

e841599feat: expired options tracking + exercise history + exit reason fix3 months ago
5105813fix: trading — correct account endpoint, LIMIT-only, qty validation3 months ago
38ab281feat: Semi-auto Trading + Positions tab (Task 3.1 + 3.2)3 months ago
Show last diff
Loading...