Initial commit: Mantle AI Trading Bot
Features: - AI-powered signal generation with multi-factor analysis - Fundamental news aggregation from multiple sources - Technical analysis with 6+ indicators - VectorDB integration for semantic search - Backtesting engine with performance metrics - Demo/paper trading mode - Real-time WebSocket updates - Comprehensive dashboard UI Built for Mantle Turing Test Hackathon - AI Trading track - AI Alpha & Data track
This commit is contained in:
552
src/lib/trading/demo/demo-trader.ts
Normal file
552
src/lib/trading/demo/demo-trader.ts
Normal file
@@ -0,0 +1,552 @@
|
||||
/**
|
||||
* Demo Trading Mode for Mantle AI Trading Bot
|
||||
* Paper trading system for testing signals without real money
|
||||
*/
|
||||
|
||||
import {
|
||||
Signal,
|
||||
Position,
|
||||
Order,
|
||||
Portfolio,
|
||||
TradeAction,
|
||||
OrderType,
|
||||
OrderStatus,
|
||||
Ticker
|
||||
} from '../core/types';
|
||||
|
||||
export interface DemoOrder {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: TradeAction;
|
||||
type: OrderType;
|
||||
quantity: number;
|
||||
price: number;
|
||||
leverage: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
status: OrderStatus;
|
||||
filledAt?: Date;
|
||||
closedAt?: Date;
|
||||
pnl?: number;
|
||||
signalId?: string;
|
||||
}
|
||||
|
||||
export interface DemoPosition {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: 'LONG' | 'SHORT';
|
||||
quantity: number;
|
||||
avgEntryPrice: number;
|
||||
currentPrice: number;
|
||||
marketValue: number;
|
||||
unrealizedPnL: number;
|
||||
unrealizedPnLPercent: number;
|
||||
leverage: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
openedAt: Date;
|
||||
signalId?: string;
|
||||
}
|
||||
|
||||
export class DemoTrader {
|
||||
private portfolio: Portfolio;
|
||||
private positions: Map<string, DemoPosition> = new Map();
|
||||
private orders: DemoOrder[] = [];
|
||||
private tradeHistory: DemoOrder[] = [];
|
||||
private currentPrices: Map<string, number> = new Map();
|
||||
private listeners: Set<(event: string, data: unknown) => void> = new Set();
|
||||
|
||||
constructor(initialCapital: number = 10000) {
|
||||
this.portfolio = {
|
||||
id: 'demo-portfolio',
|
||||
name: 'Demo Portfolio',
|
||||
totalValue: initialCapital,
|
||||
cashBalance: initialCapital,
|
||||
realizedPnL: 0,
|
||||
unrealizedPnL: 0,
|
||||
positions: [],
|
||||
demo: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to events
|
||||
*/
|
||||
subscribe(callback: (event: string, data: unknown) => void): () => void {
|
||||
this.listeners.add(callback);
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event
|
||||
*/
|
||||
private emit(event: string, data: unknown): void {
|
||||
this.listeners.forEach(cb => cb(event, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current portfolio state
|
||||
*/
|
||||
getPortfolio(): Portfolio {
|
||||
this.updatePortfolioValue();
|
||||
return { ...this.portfolio };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all positions
|
||||
*/
|
||||
getPositions(): DemoPosition[] {
|
||||
return Array.from(this.positions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get open orders
|
||||
*/
|
||||
getOpenOrders(): DemoOrder[] {
|
||||
return this.orders.filter(o => o.status === OrderStatus.OPEN || o.status === OrderStatus.PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trade history
|
||||
*/
|
||||
getTradeHistory(): DemoOrder[] {
|
||||
return [...this.tradeHistory];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current price for a symbol
|
||||
*/
|
||||
updatePrice(symbol: string, price: number): void {
|
||||
this.currentPrices.set(symbol, price);
|
||||
|
||||
// Update position values
|
||||
const position = this.positions.get(symbol);
|
||||
if (position) {
|
||||
position.currentPrice = price;
|
||||
position.marketValue = position.quantity * price;
|
||||
|
||||
if (position.side === 'LONG') {
|
||||
position.unrealizedPnL = (price - position.avgEntryPrice) * position.quantity;
|
||||
} else {
|
||||
position.unrealizedPnL = (position.avgEntryPrice - price) * position.quantity;
|
||||
}
|
||||
|
||||
position.unrealizedPnLPercent = (position.unrealizedPnL / (position.avgEntryPrice * position.quantity)) * 100;
|
||||
|
||||
// Check stop loss and take profit
|
||||
this.checkStopLevels(position);
|
||||
}
|
||||
|
||||
// Check pending orders
|
||||
this.checkPendingOrders(symbol, price);
|
||||
|
||||
this.emit('price_update', { symbol, price });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple prices at once
|
||||
*/
|
||||
updatePrices(tickers: Ticker[]): void {
|
||||
tickers.forEach(ticker => {
|
||||
this.updatePrice(ticker.symbol, ticker.lastPrice);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a demo order
|
||||
*/
|
||||
placeOrder(params: {
|
||||
symbol: string;
|
||||
side: TradeAction;
|
||||
type: OrderType;
|
||||
quantity: number;
|
||||
price?: number;
|
||||
leverage?: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
signalId?: string;
|
||||
}): DemoOrder {
|
||||
const currentPrice = this.currentPrices.get(params.symbol) || 0;
|
||||
const executionPrice = params.type === OrderType.LIMIT && params.price
|
||||
? params.price
|
||||
: currentPrice;
|
||||
|
||||
// Check if we have enough capital
|
||||
const requiredCapital = executionPrice * params.quantity * (params.leverage || 1);
|
||||
if (params.side === TradeAction.BUY && requiredCapital > this.portfolio.cashBalance) {
|
||||
throw new Error('Insufficient capital for this order');
|
||||
}
|
||||
|
||||
const order: DemoOrder = {
|
||||
id: `demo-order-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
type: params.type,
|
||||
quantity: params.quantity,
|
||||
price: executionPrice,
|
||||
leverage: params.leverage || 1,
|
||||
stopLoss: params.stopLoss,
|
||||
takeProfit: params.takeProfit,
|
||||
status: params.type === OrderType.MARKET ? OrderStatus.FILLED : OrderStatus.PENDING,
|
||||
signalId: params.signalId
|
||||
};
|
||||
|
||||
// Execute market orders immediately
|
||||
if (params.type === OrderType.MARKET) {
|
||||
this.executeOrder(order);
|
||||
} else {
|
||||
this.orders.push(order);
|
||||
}
|
||||
|
||||
this.emit('order_placed', order);
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a signal
|
||||
*/
|
||||
async executeSignal(signal: Signal): Promise<DemoOrder | null> {
|
||||
if (signal.action === TradeAction.HOLD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Calculate position size based on confidence
|
||||
const baseRisk = 0.02; // 2% risk per trade
|
||||
const riskMultiplier = signal.confidence * 2; // Scale with confidence
|
||||
const riskAmount = this.portfolio.totalValue * baseRisk * riskMultiplier;
|
||||
|
||||
const currentPrice = this.currentPrices.get(signal.symbol) || signal.priceTarget || 0;
|
||||
if (currentPrice === 0) {
|
||||
throw new Error('No price available for this symbol');
|
||||
}
|
||||
|
||||
const quantity = riskAmount / currentPrice;
|
||||
|
||||
return this.placeOrder({
|
||||
symbol: signal.symbol,
|
||||
side: signal.action,
|
||||
type: OrderType.MARKET,
|
||||
quantity: Math.floor(quantity * 100000000) / 100000000, // Round to 8 decimals
|
||||
stopLoss: signal.stopLoss,
|
||||
takeProfit: signal.takeProfit,
|
||||
signalId: signal.id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error executing signal:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a position
|
||||
*/
|
||||
closePosition(symbol: string, quantity?: number): DemoOrder | null {
|
||||
const position = this.positions.get(symbol);
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const closeQuantity = quantity || position.quantity;
|
||||
const closeSide = position.side === 'LONG' ? TradeAction.SELL : TradeAction.BUY;
|
||||
|
||||
const order = this.placeOrder({
|
||||
symbol,
|
||||
side: closeSide,
|
||||
type: OrderType.MARKET,
|
||||
quantity: closeQuantity
|
||||
});
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an order
|
||||
*/
|
||||
cancelOrder(orderId: string): boolean {
|
||||
const orderIndex = this.orders.findIndex(o => o.id === orderId);
|
||||
if (orderIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const order = this.orders[orderIndex];
|
||||
if (order.status !== OrderStatus.PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
order.status = OrderStatus.CANCELLED;
|
||||
this.orders.splice(orderIndex, 1);
|
||||
this.tradeHistory.push(order);
|
||||
|
||||
this.emit('order_cancelled', order);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an order
|
||||
*/
|
||||
private executeOrder(order: DemoOrder): void {
|
||||
const existingPosition = this.positions.get(order.symbol);
|
||||
|
||||
if (order.side === TradeAction.BUY) {
|
||||
if (existingPosition && existingPosition.side === 'LONG') {
|
||||
// Add to existing long position
|
||||
const newQuantity = existingPosition.quantity + order.quantity;
|
||||
const newAvgPrice = (
|
||||
(existingPosition.avgEntryPrice * existingPosition.quantity) +
|
||||
(order.price * order.quantity)
|
||||
) / newQuantity;
|
||||
|
||||
existingPosition.quantity = newQuantity;
|
||||
existingPosition.avgEntryPrice = newAvgPrice;
|
||||
existingPosition.marketValue = newQuantity * order.price;
|
||||
} else if (existingPosition && existingPosition.side === 'SHORT') {
|
||||
// Close short position
|
||||
this.closeExistingPosition(existingPosition, order);
|
||||
} else {
|
||||
// Create new long position
|
||||
const newPosition: DemoPosition = {
|
||||
id: `pos-${order.symbol}`,
|
||||
symbol: order.symbol,
|
||||
side: 'LONG',
|
||||
quantity: order.quantity,
|
||||
avgEntryPrice: order.price,
|
||||
currentPrice: order.price,
|
||||
marketValue: order.quantity * order.price,
|
||||
unrealizedPnL: 0,
|
||||
unrealizedPnLPercent: 0,
|
||||
leverage: order.leverage,
|
||||
stopLoss: order.stopLoss,
|
||||
takeProfit: order.takeProfit,
|
||||
openedAt: new Date(),
|
||||
signalId: order.signalId
|
||||
};
|
||||
this.positions.set(order.symbol, newPosition);
|
||||
}
|
||||
|
||||
// Deduct from cash
|
||||
this.portfolio.cashBalance -= order.price * order.quantity;
|
||||
} else if (order.side === TradeAction.SELL) {
|
||||
if (existingPosition && existingPosition.side === 'SHORT') {
|
||||
// Add to existing short position
|
||||
const newQuantity = existingPosition.quantity + order.quantity;
|
||||
const newAvgPrice = (
|
||||
(existingPosition.avgEntryPrice * existingPosition.quantity) +
|
||||
(order.price * order.quantity)
|
||||
) / newQuantity;
|
||||
|
||||
existingPosition.quantity = newQuantity;
|
||||
existingPosition.avgEntryPrice = newAvgPrice;
|
||||
} else if (existingPosition && existingPosition.side === 'LONG') {
|
||||
// Close long position
|
||||
this.closeExistingPosition(existingPosition, order);
|
||||
} else {
|
||||
// Create new short position
|
||||
const newPosition: DemoPosition = {
|
||||
id: `pos-${order.symbol}`,
|
||||
symbol: order.symbol,
|
||||
side: 'SHORT',
|
||||
quantity: order.quantity,
|
||||
avgEntryPrice: order.price,
|
||||
currentPrice: order.price,
|
||||
marketValue: order.quantity * order.price,
|
||||
unrealizedPnL: 0,
|
||||
unrealizedPnLPercent: 0,
|
||||
leverage: order.leverage,
|
||||
stopLoss: order.stopLoss,
|
||||
takeProfit: order.takeProfit,
|
||||
openedAt: new Date(),
|
||||
signalId: order.signalId
|
||||
};
|
||||
this.positions.set(order.symbol, newPosition);
|
||||
}
|
||||
|
||||
// Add to cash (for shorts, we receive cash)
|
||||
this.portfolio.cashBalance += order.price * order.quantity;
|
||||
}
|
||||
|
||||
order.status = OrderStatus.FILLED;
|
||||
order.filledAt = new Date();
|
||||
this.tradeHistory.push(order);
|
||||
|
||||
this.emit('order_filled', order);
|
||||
this.emit('position_updated', this.positions.get(order.symbol));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close existing position
|
||||
*/
|
||||
private closeExistingPosition(position: DemoPosition, order: DemoOrder): void {
|
||||
const closeQuantity = Math.min(position.quantity, order.quantity);
|
||||
|
||||
// Calculate realized PnL
|
||||
let pnl: number;
|
||||
if (position.side === 'LONG') {
|
||||
pnl = (order.price - position.avgEntryPrice) * closeQuantity;
|
||||
} else {
|
||||
pnl = (position.avgEntryPrice - order.price) * closeQuantity;
|
||||
}
|
||||
|
||||
// Update order PnL
|
||||
order.pnl = pnl;
|
||||
order.closedAt = new Date();
|
||||
|
||||
// Update portfolio
|
||||
this.portfolio.realizedPnL += pnl;
|
||||
if (position.side === 'LONG') {
|
||||
this.portfolio.cashBalance += order.price * closeQuantity;
|
||||
}
|
||||
|
||||
// Update or remove position
|
||||
if (closeQuantity >= position.quantity) {
|
||||
this.positions.delete(position.symbol);
|
||||
this.emit('position_closed', position);
|
||||
} else {
|
||||
position.quantity -= closeQuantity;
|
||||
position.marketValue = position.quantity * order.price;
|
||||
}
|
||||
|
||||
// Update signal result if applicable
|
||||
if (position.signalId) {
|
||||
this.emit('signal_result', {
|
||||
signalId: position.signalId,
|
||||
pnl,
|
||||
result: pnl > 0 ? 'WIN' : pnl < 0 ? 'LOSS' : 'NEUTRAL'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stop loss and take profit levels
|
||||
*/
|
||||
private checkStopLevels(position: DemoPosition): void {
|
||||
if (position.stopLoss && position.currentPrice <= position.stopLoss && position.side === 'LONG') {
|
||||
this.closePosition(position.symbol);
|
||||
this.emit('stop_loss_triggered', position);
|
||||
}
|
||||
|
||||
if (position.stopLoss && position.currentPrice >= position.stopLoss && position.side === 'SHORT') {
|
||||
this.closePosition(position.symbol);
|
||||
this.emit('stop_loss_triggered', position);
|
||||
}
|
||||
|
||||
if (position.takeProfit && position.currentPrice >= position.takeProfit && position.side === 'LONG') {
|
||||
this.closePosition(position.symbol);
|
||||
this.emit('take_profit_triggered', position);
|
||||
}
|
||||
|
||||
if (position.takeProfit && position.currentPrice <= position.takeProfit && position.side === 'SHORT') {
|
||||
this.closePosition(position.symbol);
|
||||
this.emit('take_profit_triggered', position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check pending limit orders
|
||||
*/
|
||||
private checkPendingOrders(symbol: string, price: number): void {
|
||||
this.orders.forEach(order => {
|
||||
if (order.symbol !== symbol || order.status !== OrderStatus.PENDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (order.type === OrderType.LIMIT) {
|
||||
// For buy limit, execute when price drops to or below limit price
|
||||
if (order.side === TradeAction.BUY && price <= order.price) {
|
||||
this.executeOrder(order);
|
||||
}
|
||||
// For sell limit, execute when price rises to or above limit price
|
||||
if (order.side === TradeAction.SELL && price >= order.price) {
|
||||
this.executeOrder(order);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update portfolio value
|
||||
*/
|
||||
private updatePortfolioValue(): void {
|
||||
let totalUnrealizedPnL = 0;
|
||||
|
||||
this.positions.forEach(position => {
|
||||
totalUnrealizedPnL += position.unrealizedPnL;
|
||||
});
|
||||
|
||||
this.portfolio.unrealizedPnL = totalUnrealizedPnL;
|
||||
this.portfolio.totalValue = this.portfolio.cashBalance + totalUnrealizedPnL;
|
||||
this.portfolio.positions = Array.from(this.positions.values()).map(p => ({
|
||||
id: p.id,
|
||||
symbol: p.symbol,
|
||||
side: p.side,
|
||||
quantity: p.quantity,
|
||||
avgEntryPrice: p.avgEntryPrice,
|
||||
currentPrice: p.currentPrice,
|
||||
marketValue: p.marketValue,
|
||||
unrealizedPnL: p.unrealizedPnL,
|
||||
unrealizedPnLPercent: p.unrealizedPnLPercent,
|
||||
leverage: p.leverage,
|
||||
openedAt: p.openedAt,
|
||||
demo: true
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset demo account
|
||||
*/
|
||||
reset(initialCapital: number = 10000): void {
|
||||
this.positions.clear();
|
||||
this.orders = [];
|
||||
this.tradeHistory = [];
|
||||
this.portfolio = {
|
||||
id: 'demo-portfolio',
|
||||
name: 'Demo Portfolio',
|
||||
totalValue: initialCapital,
|
||||
cashBalance: initialCapital,
|
||||
realizedPnL: 0,
|
||||
unrealizedPnL: 0,
|
||||
positions: [],
|
||||
demo: true
|
||||
};
|
||||
|
||||
this.emit('portfolio_reset', this.portfolio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance statistics
|
||||
*/
|
||||
getStatistics(): {
|
||||
totalTrades: number;
|
||||
winningTrades: number;
|
||||
losingTrades: number;
|
||||
winRate: number;
|
||||
totalPnL: number;
|
||||
averagePnL: number;
|
||||
bestTrade: number;
|
||||
worstTrade: number;
|
||||
profitFactor: number;
|
||||
} {
|
||||
const trades = this.tradeHistory.filter(t => t.pnl !== undefined);
|
||||
const wins = trades.filter(t => (t.pnl || 0) > 0);
|
||||
const losses = trades.filter(t => (t.pnl || 0) <= 0);
|
||||
|
||||
const totalPnL = trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
|
||||
const grossProfit = wins.reduce((sum, t) => sum + (t.pnl || 0), 0);
|
||||
const grossLoss = Math.abs(losses.reduce((sum, t) => sum + (t.pnl || 0), 0));
|
||||
|
||||
return {
|
||||
totalTrades: trades.length,
|
||||
winningTrades: wins.length,
|
||||
losingTrades: losses.length,
|
||||
winRate: trades.length > 0 ? (wins.length / trades.length) * 100 : 0,
|
||||
totalPnL,
|
||||
averagePnL: trades.length > 0 ? totalPnL / trades.length : 0,
|
||||
bestTrade: trades.length > 0 ? Math.max(...trades.map(t => t.pnl || 0)) : 0,
|
||||
worstTrade: trades.length > 0 ? Math.min(...trades.map(t => t.pnl || 0)) : 0,
|
||||
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton
|
||||
export const demoTrader = new DemoTrader();
|
||||
Reference in New Issue
Block a user