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,443 @@
/**
* Vector Database Service for Mantle AI Trading Bot
* ChromaDB integration for semantic search of news and analysis
*/
import { ChromaClient, Collection, IncludeEnum } from 'chromadb-client';
import { NewsArticle, Signal, SignalAnalysis, SentimentLabel } from '../trading/core/types';
// Collection names
const COLLECTIONS = {
NEWS: 'trading_news',
SIGNALS: 'trading_signals',
ANALYSIS: 'signal_analysis'
};
// Embedding dimension (for typical embedding models)
const EMBEDDING_DIMENSION = 384;
export class VectorStore {
private client: ChromaClient | null = null;
private newsCollection: Collection | null = null;
private signalsCollection: Collection | null = null;
private analysisCollection: Collection | null = null;
private connected = false;
constructor() {
this.init();
}
/**
* Initialize ChromaDB connection
*/
private async init(): Promise<void> {
try {
// Try to connect to ChromaDB server
this.client = new ChromaClient({
path: process.env.CHROMADB_URL || 'http://localhost:8000'
});
// Create or get collections
await this.createCollections();
this.connected = true;
console.log('VectorStore: Connected to ChromaDB');
} catch (error) {
console.warn('VectorStore: ChromaDB not available, using fallback mode');
this.connected = false;
}
}
/**
* Create or get collections
*/
private async createCollections(): Promise<void> {
if (!this.client) return;
// News collection
this.newsCollection = await this.client.getOrCreateCollection({
name: COLLECTIONS.NEWS,
metadata: { description: 'Trading news articles for semantic search' }
});
// Signals collection
this.signalsCollection = await this.client.getOrCreateCollection({
name: COLLECTIONS.SIGNALS,
metadata: { description: 'Historical trading signals' }
});
// Analysis collection
this.analysisCollection = await this.client.getOrCreateCollection({
name: COLLECTIONS.ANALYSIS,
metadata: { description: 'Signal analysis and reasoning' }
});
}
/**
* Check if connected
*/
isConnected(): boolean {
return this.connected;
}
/**
* Generate simple embedding (fallback when no embedding model)
* This creates a deterministic embedding based on text content
*/
private generateSimpleEmbedding(text: string): number[] {
const embedding = new Array(EMBEDDING_DIMENSION).fill(0);
const words = text.toLowerCase().split(/\s+/);
words.forEach((word, index) => {
// Simple hash-based embedding
const hash = this.simpleHash(word);
const pos = Math.abs(hash) % EMBEDDING_DIMENSION;
embedding[pos] += 1;
// Add positional encoding
const pos2 = (pos + index) % EMBEDDING_DIMENSION;
embedding[pos2] += 0.5;
});
// Normalize
const norm = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0)) || 1;
return embedding.map(val => val / norm);
}
/**
* Simple string hash
*/
private simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
/**
* Store news article in vector database
*/
async storeNewsArticle(article: NewsArticle): Promise<string | null> {
if (!this.newsCollection || !this.connected) {
// Store article ID as vector reference
return null;
}
try {
const id = article.id || `news-${Date.now()}`;
const text = `${article.title} ${article.content || ''}`;
const embedding = this.generateSimpleEmbedding(text);
await this.newsCollection.add({
ids: [id],
embeddings: [embedding],
metadatas: [{
title: article.title,
source: article.source,
category: article.category || 'General',
sentiment: article.sentiment || 0,
importance: article.importance || 0.5,
publishedAt: article.publishedAt?.toISOString() || new Date().toISOString(),
url: article.sourceUrl || ''
}],
documents: [text]
});
return id;
} catch (error) {
console.error('Error storing news article:', error);
return null;
}
}
/**
* Store multiple news articles
*/
async storeNewsArticles(articles: NewsArticle[]): Promise<string[]> {
if (!this.newsCollection || !this.connected) {
return [];
}
const ids: string[] = [];
const embeddings: number[][] = [];
const metadatas: Record<string, unknown>[] = [];
const documents: string[] = [];
articles.forEach(article => {
const id = article.id || `news-${Date.now()}-${Math.random()}`;
const text = `${article.title} ${article.content || ''}`;
ids.push(id);
embeddings.push(this.generateSimpleEmbedding(text));
metadatas.push({
title: article.title,
source: article.source,
category: article.category || 'General',
sentiment: article.sentiment || 0,
importance: article.importance || 0.5,
publishedAt: article.publishedAt?.toISOString() || new Date().toISOString(),
url: article.sourceUrl || ''
});
documents.push(text);
});
try {
await this.newsCollection.add({
ids,
embeddings,
metadatas,
documents
});
return ids;
} catch (error) {
console.error('Error storing news articles:', error);
return [];
}
}
/**
* Search similar news articles
*/
async searchSimilarNews(
query: string,
nResults: number = 10,
filters?: Record<string, unknown>
): Promise<Array<{
id: string;
text: string;
metadata: Record<string, unknown>;
distance: number;
}>> {
if (!this.newsCollection || !this.connected) {
return [];
}
try {
const queryEmbedding = this.generateSimpleEmbedding(query);
const results = await this.newsCollection.query({
queryEmbeddings: [queryEmbedding],
nResults,
where: filters,
include: [IncludeEnum.Documents, IncludeEnum.Metadatas, IncludeEnum.Distances]
});
if (!results.ids[0]) return [];
return results.ids[0].map((id, index) => ({
id,
text: results.documents?.[0]?.[index] || '',
metadata: results.metadatas?.[0]?.[index] || {},
distance: results.distances?.[0]?.[index] || 0
}));
} catch (error) {
console.error('Error searching news:', error);
return [];
}
}
/**
* Store signal with analysis
*/
async storeSignalAnalysis(
signal: Signal,
analysis: SignalAnalysis
): Promise<void> {
if (!this.signalsCollection || !this.analysisCollection || !this.connected) {
return;
}
try {
// Store signal
const signalText = `${signal.symbol} ${signal.action} ${signal.reasoning}`;
await this.signalsCollection.add({
ids: [signal.id],
embeddings: [this.generateSimpleEmbedding(signalText)],
metadatas: [{
symbol: signal.symbol,
action: signal.action,
confidence: signal.confidence,
status: signal.status,
result: signal.result || 'PENDING',
pnl: signal.resultPnL || 0,
createdAt: signal.createdAt.toISOString()
}],
documents: [signalText]
});
// Store analysis
const analysisText = analysis.keyFactors.join(' ') + ' ' +
analysis.technicalAnalysis.patterns.join(' ') + ' ' +
analysis.fundamentalAnalysis.marketEvents.join(' ');
await this.analysisCollection.add({
ids: [`analysis-${signal.id}`],
embeddings: [this.generateSimpleEmbedding(analysisText)],
metadatas: [{
signalId: signal.id,
overallScore: analysis.overallScore,
technicalScore: analysis.technicalAnalysis.score,
fundamentalScore: analysis.fundamentalAnalysis.score,
sentimentScore: analysis.sentimentAnalysis.overallSentiment
}],
documents: [analysisText]
});
} catch (error) {
console.error('Error storing signal analysis:', error);
}
}
/**
* Find similar historical signals
*/
async findSimilarSignals(
symbol: string,
action: string,
reasoning: string,
nResults: number = 5
): Promise<Array<{
signalId: string;
metadata: Record<string, unknown>;
distance: number;
}>> {
if (!this.signalsCollection || !this.connected) {
return [];
}
try {
const queryText = `${symbol} ${action} ${reasoning}`;
const queryEmbedding = this.generateSimpleEmbedding(queryText);
const results = await this.signalsCollection.query({
queryEmbeddings: [queryEmbedding],
nResults,
where: { symbol },
include: [IncludeEnum.Metadatas, IncludeEnum.Distances]
});
if (!results.ids[0]) return [];
return results.ids[0].map((id, index) => ({
signalId: id,
metadata: results.metadatas?.[0]?.[index] || {},
distance: results.distances?.[0]?.[index] || 0
}));
} catch (error) {
console.error('Error finding similar signals:', error);
return [];
}
}
/**
* Get signal statistics from vector store
*/
async getSignalStatistics(symbol?: string): Promise<{
totalSignals: number;
winRate: number;
avgConfidence: number;
avgPnL: number;
}> {
// This would require aggregation queries in ChromaDB
// For now, return placeholder values
return {
totalSignals: 0,
winRate: 0,
avgConfidence: 0,
avgPnL: 0
};
}
/**
* Delete old entries (cleanup)
*/
async cleanupOldEntries(beforeDate: Date): Promise<void> {
if (!this.connected) return;
try {
// ChromaDB doesn't have a direct delete by date,
// would need to query and delete by IDs
// This is a placeholder for cleanup logic
} catch (error) {
console.error('Error during cleanup:', error);
}
}
/**
* Get contextual news for signal generation
*/
async getContextualNews(
symbol: string,
timeframe: string,
maxArticles: number = 10
): Promise<NewsArticle[]> {
// Search for relevant news
const results = await this.searchSimilarNews(
`${symbol} cryptocurrency trading`,
maxArticles,
{ symbol: { $contains: symbol } }
);
return results.map(result => ({
id: result.id,
title: result.metadata.title as string || '',
source: result.metadata.source as string || 'Unknown',
sentiment: result.metadata.sentiment as number,
importance: result.metadata.importance as number,
category: result.metadata.category as string,
publishedAt: new Date(result.metadata.publishedAt as string),
fetchedAt: new Date(),
processed: true,
vectorId: result.id
}));
}
/**
* Analyze text sentiment using stored knowledge
*/
async analyzeSentimentWithContext(text: string): Promise<{
sentiment: number;
label: SentimentLabel;
relevantArticles: NewsArticle[];
}> {
// Search for similar articles
const similar = await this.searchSimilarNews(text, 5);
// Calculate aggregate sentiment from similar articles
let totalSentiment = 0;
const relevantArticles: NewsArticle[] = [];
similar.forEach(result => {
const sentiment = result.metadata.sentiment as number || 0;
totalSentiment += sentiment;
relevantArticles.push({
id: result.id,
title: result.metadata.title as string || '',
source: result.metadata.source as string || 'Unknown',
sentiment,
publishedAt: new Date(result.metadata.publishedAt as string),
fetchedAt: new Date(),
processed: true
});
});
const avgSentiment = similar.length > 0 ? totalSentiment / similar.length : 0;
let label = SentimentLabel.NEUTRAL;
if (avgSentiment >= 0.3) label = SentimentLabel.BULLISH;
else if (avgSentiment >= 0.6) label = SentimentLabel.VERY_BULLISH;
else if (avgSentiment <= -0.3) label = SentimentLabel.BEARISH;
else if (avgSentiment <= -0.6) label = SentimentLabel.VERY_BEARISH;
return {
sentiment: avgSentiment,
label,
relevantArticles
};
}
}
// Export singleton
export const vectorStore = new VectorStore();