โ† ะะฐะทะฐะด
'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;