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:
Mantle AI Trader
2026-06-06 06:02:07 +00:00
Unverified
parent 6664758a6d
commit b1da4ee01d
100 changed files with 16113 additions and 0 deletions

View 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();