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