← Back
'use strict';

const express = require('express');
const config = require('../config');
const { checkApiKey } = require('../middleware/auth');
const trading = require('../services/trading');
const { openTradeWithTP, getTrades, MAX_POSITION_COST } = require('../services/tradeManager');
const tradeLogger = require('../services/tradeLogger');
const logger = require('../utils/logger');

const router = express.Router();

// ─── Safety middleware: block if trading disabled ─────────
function requireTrading(req, res, next) {
  if (!config.trading.enabled) {
    return res.status(403).json({
      success: false,
      error: 'Trading is disabled. Set TRADING_ENABLED=true to enable.',
    });
  }
  next();
}

// ─── GET /api/trading/status ─────────────────────────────
// Check if trading is enabled + account summary
router.get('/api/trading/status', checkApiKey, async (req, res) => {
  try {
    const status = {
      enabled: config.trading.enabled,
      hasCredentials: !!(config.binance.apiKey && config.binance.apiSecret),
    };

    if (config.trading.enabled) {
      try {
        const account = await trading.getAccount();
        status.account = {
          balance: account.asset || [],
          // Filter to show USDT balance
          usdtBalance: (account.asset || []).find(a => a.asset === 'USDT'),
        };
      } catch (err) {
        status.accountError = err.message;
      }
    }

    res.json({ success: true, data: status });
  } catch (err) {
    logger.error(`GET /api/trading/status error: ${err.message}`);
    res.status(500).json({ success: false, error: err.message });
  }
});

// ─── GET /api/trading/positions ──────────────────────────
// Open options positions
router.get('/api/trading/positions', checkApiKey, requireTrading, async (req, res) => {
  try {
    const positions = await trading.getPositions();
    // Filter non-zero positions
    const active = (positions || []).filter(p =>
      parseFloat(p.quantity || 0) !== 0
    );
    res.json({ success: true, data: active });
  } catch (err) {
    logger.error(`GET /api/trading/positions error: ${err.message}`);
    res.status(500).json({ success: false, error: err.message });
  }
});

// ─── GET /api/trading/orders ─────────────────────────────
// Open orders (optionally filtered by symbol)
router.get('/api/trading/orders', checkApiKey, requireTrading, async (req, res) => {
  try {
    const orders = await trading.getOpenOrders(req.query.symbol);
    res.json({ success: true, data: orders });
  } catch (err) {
    logger.error(`GET /api/trading/orders error: ${err.message}`);
    res.status(500).json({ success: false, error: err.message });
  }
});

// ─── POST /api/trading/order ─────────────────────────────
// Place a new order
// Body: { symbol, side, type, quantity, price, reduceOnly }
router.post('/api/trading/order', checkApiKey, requireTrading, async (req, res) => {
  try {
    const { symbol, side, quantity, price, reduceOnly, tpPct } = req.body;

    // Validation
    if (!symbol || !side || !quantity || !price) {
      return res.status(400).json({ success: false, error: 'Required: symbol, side, quantity, price (LIMIT only)' });
    }
    if (!['BUY', 'SELL'].includes(side.toUpperCase())) {
      return res.status(400).json({ success: false, error: 'Side must be BUY or SELL' });
    }
    if (parseFloat(quantity) < 0.01) {
      return res.status(400).json({ success: false, error: 'Min quantity is 0.01' });
    }
    if (parseFloat(price) <= 0) {
      return res.status(400).json({ success: false, error: 'Price must be positive' });
    }

    const qty = parseFloat(quantity);
    const prc = parseFloat(price);

    // $5 cap for BUY orders
    if (side.toUpperCase() === 'BUY') {
      const cost = qty * prc;
      if (cost > MAX_POSITION_COST) {
        return res.status(400).json({ success: false, error: `Cost $${cost.toFixed(2)} exceeds $${MAX_POSITION_COST} limit` });
      }
    }

    let result;
    if (side.toUpperCase() === 'BUY' && tpPct) {
      // Auto-TP flow: BUY + monitor fill + place SELL at TP
      result = await openTradeWithTP({ symbol, quantity: qty, price: prc, tpPct });
    } else {
      // Direct order (SELL / manual)
      result = await trading.placeOrder({
        symbol,
        side: side.toUpperCase(),
        quantity: qty,
        price: prc,
        reduceOnly: reduceOnly || false,
      });
    }

    res.json({ success: true, data: result });
  } catch (err) {
    logger.error(`POST /api/trading/order error: ${err.response?.data?.msg || err.message}`);
    res.status(err.response?.status || 500).json({
      success: false,
      error: err.response?.data?.msg || err.message,
    });
  }
});

// ─── DELETE /api/trading/order ────────────────────────────
// Cancel an order
// Query: symbol, orderId
router.delete('/api/trading/order', checkApiKey, requireTrading, async (req, res) => {
  try {
    const { symbol, orderId } = req.query;
    if (!symbol || !orderId) {
      return res.status(400).json({ success: false, error: 'Required: symbol, orderId' });
    }

    const result = await trading.cancelOrder(symbol, orderId);
    res.json({ success: true, data: result });
  } catch (err) {
    logger.error(`DELETE /api/trading/order error: ${err.response?.data?.msg || err.message}`);
    res.status(err.response?.status || 500).json({
      success: false,
      error: err.response?.data?.msg || err.message,
    });
  }
});

// ─── DELETE /api/trading/orders ───────────────────────────
// Cancel all orders for a symbol
router.delete('/api/trading/orders', checkApiKey, requireTrading, async (req, res) => {
  try {
    const { symbol } = req.query;
    if (!symbol) {
      return res.status(400).json({ success: false, error: 'Required: symbol' });
    }

    const result = await trading.cancelAllOrders(symbol);
    res.json({ success: true, data: result });
  } catch (err) {
    logger.error(`DELETE /api/trading/orders error: ${err.response?.data?.msg || err.message}`);
    res.status(err.response?.status || 500).json({
      success: false,
      error: err.response?.data?.msg || err.message,
    });
  }
});

// ─── GET /api/trading/history ────────────────────────────
// Trade history for a symbol
router.get('/api/trading/history', checkApiKey, requireTrading, async (req, res) => {
  try {
    const { symbol, limit } = req.query;
    if (!symbol) {
      return res.status(400).json({ success: false, error: 'Required: symbol' });
    }

    const trades = await trading.getTradeHistory(symbol, parseInt(limit) || 20);
    res.json({ success: true, data: trades });
  } catch (err) {
    logger.error(`GET /api/trading/history error: ${err.response?.data?.msg || err.message}`);
    res.status(err.response?.status || 500).json({
      success: false,
      error: err.response?.data?.msg || err.message,
    });
  }
});

// ─── GET /api/trading/active ─────────────────────────────
// Active trades managed by tradeManager (with auto-TP)
router.get('/api/trading/active', checkApiKey, requireTrading, async (req, res) => {
  try {
    res.json({ success: true, data: getTrades() });
  } catch (err) {
    res.status(500).json({ success: false, error: err.message });
  }
});

// ─── POST /api/trading/sync ─────────────────────────────
// Sync trade history from Binance into local journal
router.post('/api/trading/sync', checkApiKey, requireTrading, async (req, res) => {
  try {
    const results = await tradeLogger.syncFromBinance();
    res.json({ success: true, data: results });
  } catch (err) {
    logger.error(`POST /api/trading/sync error: ${err.message}`);
    res.status(500).json({ success: false, error: err.message });
  }
});

// ─── GET /api/trading/stats ──────────────────────────────
// Aggregated trading statistics
router.get('/api/trading/stats', checkApiKey, async (req, res) => {
  try {
    const { days, underlying } = req.query;
    const stats = await tradeLogger.getStats({
      days: days ? parseInt(days) : undefined,
      underlying: underlying || undefined,
    });
    res.json({ success: true, data: stats });
  } catch (err) {
    logger.error(`GET /api/trading/stats error: ${err.message}`);
    res.status(500).json({ success: false, error: err.message });
  }
});

// ─── GET /api/trading/journal ───────────────────────────
// Paginated trade journal (closed trades)
router.get('/api/trading/journal', checkApiKey, async (req, res) => {
  try {
    const { days, underlying, limit, offset } = req.query;
    const journal = await tradeLogger.getJournal({
      days: days ? parseInt(days) : undefined,
      underlying: underlying || undefined,
      limit: limit ? parseInt(limit) : 50,
      offset: offset ? parseInt(offset) : 0,
    });
    res.json({ success: true, data: journal });
  } catch (err) {
    logger.error(`GET /api/trading/journal error: ${err.message}`);
    res.status(500).json({ success: false, error: err.message });
  }
});

module.exports = router;

📜 Git History

43817b9feat: trading stats + trade journal — Binance sync, stats bar, journal table3 months ago
7bd5a13feat: auto-TP trade manager + $5 position cap3 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...