clean up
This commit is contained in:
parent
7993148a95
commit
528be93804
38 changed files with 4617 additions and 1081 deletions
29
apps/data-service/package.json
Normal file
29
apps/data-service/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@stock-bot/data-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Combined data ingestion and historical data service",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target node",
|
||||
"start": "bun dist/index.js",
|
||||
"test": "bun test",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/types": "workspace:*",
|
||||
"@stock-bot/questdb-client": "workspace:*",
|
||||
"@stock-bot/mongodb-client": "workspace:*",
|
||||
"@stock-bot/event-bus": "workspace:*",
|
||||
"@stock-bot/http-client": "workspace:*",
|
||||
"hono": "^4.0.0",
|
||||
"ws": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
59
apps/data-service/src/index.ts
Normal file
59
apps/data-service/src/index.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Data Service - Combined live and historical data ingestion
|
||||
*/
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { Hono } from 'hono';
|
||||
import { serve } from '@hono/node-server';
|
||||
|
||||
// Load environment variables
|
||||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
const logger = createLogger('data-service');
|
||||
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
service: 'data-service',
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.get('/api/live/:symbol', async (c) => {
|
||||
const symbol = c.req.param('symbol');
|
||||
logger.info('Live data request', { symbol });
|
||||
|
||||
// TODO: Implement live data fetching
|
||||
return c.json({
|
||||
symbol,
|
||||
message: 'Live data endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/historical/:symbol', async (c) => {
|
||||
const symbol = c.req.param('symbol');
|
||||
const from = c.req.query('from');
|
||||
const to = c.req.query('to');
|
||||
|
||||
logger.info('Historical data request', { symbol, from, to });
|
||||
|
||||
// TODO: Implement historical data fetching
|
||||
return c.json({
|
||||
symbol,
|
||||
from,
|
||||
to,
|
||||
message: 'Historical data endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: PORT,
|
||||
});
|
||||
|
||||
logger.info(`Data Service started on port ${PORT}`);
|
||||
367
apps/data-service/src/providers/unified.ts
Normal file
367
apps/data-service/src/providers/unified.ts
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
/**
|
||||
* Unified data interface for live and historical data
|
||||
*/
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { QuestDBClient } from '@stock-bot/questdb-client';
|
||||
import { EventBus } from '@stock-bot/event-bus';
|
||||
|
||||
const logger = createLogger('unified-data-provider');
|
||||
|
||||
export interface MarketData {
|
||||
symbol: string;
|
||||
timestamp: Date;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface DataProviderConfig {
|
||||
questdb?: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
enableCache?: boolean;
|
||||
cacheSize?: number;
|
||||
}
|
||||
|
||||
export interface DataProvider {
|
||||
getLiveData(symbol: string): Promise<MarketData>;
|
||||
getHistoricalData(symbol: string, from: Date, to: Date, interval?: string): Promise<MarketData[]>;
|
||||
subscribeLiveData(symbol: string, callback: (data: MarketData) => void): Promise<void>;
|
||||
unsubscribeLiveData(symbol: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class UnifiedDataProvider implements DataProvider {
|
||||
private questdb?: QuestDBClient;
|
||||
private eventBus: EventBus;
|
||||
private cache: Map<string, MarketData[]> = new Map();
|
||||
private liveSubscriptions: Map<string, ((data: MarketData) => void)[]> = new Map();
|
||||
private config: DataProviderConfig;
|
||||
|
||||
constructor(eventBus: EventBus, config: DataProviderConfig = {}) {
|
||||
this.eventBus = eventBus;
|
||||
this.config = {
|
||||
enableCache: true,
|
||||
cacheSize: 10000,
|
||||
...config
|
||||
};
|
||||
|
||||
// Initialize QuestDB client
|
||||
if (config.questdb) {
|
||||
this.questdb = new QuestDBClient({
|
||||
host: config.questdb.host,
|
||||
port: config.questdb.port
|
||||
});
|
||||
}
|
||||
|
||||
this.initializeEventSubscriptions();
|
||||
}
|
||||
|
||||
private initializeEventSubscriptions(): void {
|
||||
// Subscribe to live market data events
|
||||
this.eventBus.subscribe('market.data.live', (message) => {
|
||||
const data = message.data as MarketData;
|
||||
this.handleLiveData(data);
|
||||
});
|
||||
|
||||
// Subscribe to historical data requests
|
||||
this.eventBus.subscribe('market.data.request', async (message) => {
|
||||
const { symbol, from, to, interval, requestId } = message.data;
|
||||
try {
|
||||
const data = await this.getHistoricalData(symbol, new Date(from), new Date(to), interval);
|
||||
await this.eventBus.publish('market.data.response', {
|
||||
requestId,
|
||||
symbol,
|
||||
data,
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
await this.eventBus.publish('market.data.response', {
|
||||
requestId,
|
||||
symbol,
|
||||
error: error.message,
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getLiveData(symbol: string): Promise<MarketData> {
|
||||
logger.info('Fetching live data', { symbol });
|
||||
|
||||
try {
|
||||
// First check cache for recent data
|
||||
if (this.config.enableCache) {
|
||||
const cached = this.getFromCache(symbol);
|
||||
if (cached && this.isRecentData(cached)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// For demo purposes, generate simulated live data
|
||||
// In production, this would integrate with real market data providers
|
||||
const liveData = this.generateSimulatedData(symbol);
|
||||
|
||||
// Store in cache
|
||||
if (this.config.enableCache) {
|
||||
this.addToCache(symbol, liveData);
|
||||
}
|
||||
|
||||
// Publish live data event
|
||||
await this.eventBus.publishMarketData(symbol, liveData);
|
||||
|
||||
return liveData;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch live data', { symbol, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getHistoricalData(symbol: string, from: Date, to: Date, interval: string = '1m'): Promise<MarketData[]> {
|
||||
logger.info('Fetching historical data', { symbol, from, to, interval });
|
||||
|
||||
try {
|
||||
// Check cache first
|
||||
const cacheKey = `${symbol}_${from.getTime()}_${to.getTime()}_${interval}`;
|
||||
if (this.config.enableCache && this.cache.has(cacheKey)) {
|
||||
logger.debug('Returning cached historical data', { symbol, cacheKey });
|
||||
return this.cache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
// Try to fetch from QuestDB
|
||||
let data: MarketData[] = [];
|
||||
|
||||
if (this.questdb) {
|
||||
try {
|
||||
data = await this.fetchFromQuestDB(symbol, from, to, interval);
|
||||
} catch (error) {
|
||||
logger.warn('Failed to fetch from QuestDB, generating simulated data', { error });
|
||||
data = this.generateHistoricalData(symbol, from, to, interval);
|
||||
}
|
||||
} else {
|
||||
// Generate simulated data if no QuestDB connection
|
||||
data = this.generateHistoricalData(symbol, from, to, interval);
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
if (this.config.enableCache) {
|
||||
this.cache.set(cacheKey, data);
|
||||
this.trimCache();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch historical data', { symbol, from, to, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async subscribeLiveData(symbol: string, callback: (data: MarketData) => void): Promise<void> {
|
||||
logger.info('Subscribing to live data', { symbol });
|
||||
|
||||
if (!this.liveSubscriptions.has(symbol)) {
|
||||
this.liveSubscriptions.set(symbol, []);
|
||||
}
|
||||
|
||||
this.liveSubscriptions.get(symbol)!.push(callback);
|
||||
|
||||
// Start live data simulation for this symbol
|
||||
this.startLiveDataSimulation(symbol);
|
||||
}
|
||||
|
||||
async unsubscribeLiveData(symbol: string): Promise<void> {
|
||||
logger.info('Unsubscribing from live data', { symbol });
|
||||
this.liveSubscriptions.delete(symbol);
|
||||
}
|
||||
private async fetchFromQuestDB(symbol: string, from: Date, to: Date, interval: string): Promise<MarketData[]> {
|
||||
if (!this.questdb) {
|
||||
throw new Error('QuestDB client not initialized');
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT symbol, timestamp, open, high, low, close, volume
|
||||
FROM market_data
|
||||
WHERE symbol = '${symbol}'
|
||||
AND timestamp >= '${from.toISOString()}'
|
||||
AND timestamp <= '${to.toISOString()}'
|
||||
ORDER BY timestamp ASC
|
||||
`;
|
||||
|
||||
const result = await this.questdb.query(query);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
symbol: row.symbol,
|
||||
timestamp: new Date(row.timestamp),
|
||||
open: parseFloat(row.open),
|
||||
high: parseFloat(row.high),
|
||||
low: parseFloat(row.low),
|
||||
close: parseFloat(row.close),
|
||||
volume: parseInt(row.volume),
|
||||
source: 'questdb'
|
||||
}));
|
||||
}
|
||||
|
||||
private generateHistoricalData(symbol: string, from: Date, to: Date, interval: string): MarketData[] {
|
||||
const data: MarketData[] = [];
|
||||
const intervalMs = this.parseInterval(interval);
|
||||
const current = new Date(from);
|
||||
|
||||
let basePrice = 100; // Starting price
|
||||
|
||||
while (current <= to) {
|
||||
const timestamp = new Date(current);
|
||||
|
||||
// Generate realistic OHLCV data with some randomness
|
||||
const volatility = 0.02;
|
||||
const change = (Math.random() - 0.5) * volatility;
|
||||
|
||||
const open = basePrice;
|
||||
const close = open * (1 + change);
|
||||
const high = Math.max(open, close) * (1 + Math.random() * 0.01);
|
||||
const low = Math.min(open, close) * (1 - Math.random() * 0.01);
|
||||
const volume = Math.floor(Math.random() * 100000) + 10000;
|
||||
|
||||
data.push({
|
||||
symbol,
|
||||
timestamp,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
source: 'simulated'
|
||||
});
|
||||
|
||||
basePrice = close;
|
||||
current.setTime(current.getTime() + intervalMs);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private generateSimulatedData(symbol: string): MarketData {
|
||||
const now = new Date();
|
||||
const basePrice = 100 + Math.sin(now.getTime() / 1000000) * 10;
|
||||
const volatility = 0.001;
|
||||
|
||||
const open = basePrice + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const close = open + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const high = Math.max(open, close) + Math.random() * volatility * basePrice;
|
||||
const low = Math.min(open, close) - Math.random() * volatility * basePrice;
|
||||
const volume = Math.floor(Math.random() * 10000) + 1000;
|
||||
|
||||
return {
|
||||
symbol,
|
||||
timestamp: now,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
source: 'simulated'
|
||||
};
|
||||
}
|
||||
|
||||
private parseInterval(interval: string): number {
|
||||
const value = parseInt(interval.slice(0, -1));
|
||||
const unit = interval.slice(-1).toLowerCase();
|
||||
|
||||
switch (unit) {
|
||||
case 's': return value * 1000;
|
||||
case 'm': return value * 60 * 1000;
|
||||
case 'h': return value * 60 * 60 * 1000;
|
||||
case 'd': return value * 24 * 60 * 60 * 1000;
|
||||
default: return 60 * 1000; // Default to 1 minute
|
||||
}
|
||||
}
|
||||
|
||||
private handleLiveData(data: MarketData): void {
|
||||
const callbacks = this.liveSubscriptions.get(data.symbol);
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
logger.error('Error in live data callback', { symbol: data.symbol, error });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private startLiveDataSimulation(symbol: string): void {
|
||||
// Simulate live data updates every second
|
||||
const interval = setInterval(async () => {
|
||||
if (!this.liveSubscriptions.has(symbol)) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const liveData = this.generateSimulatedData(symbol);
|
||||
this.handleLiveData(liveData);
|
||||
await this.eventBus.publishMarketData(symbol, liveData);
|
||||
} catch (error) {
|
||||
logger.error('Error in live data simulation', { symbol, error });
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private getFromCache(symbol: string): MarketData | null {
|
||||
// Get most recent cached data for symbol
|
||||
for (const [key, data] of this.cache.entries()) {
|
||||
if (key.startsWith(symbol) && data.length > 0) {
|
||||
return data[data.length - 1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isRecentData(data: MarketData): boolean {
|
||||
const now = Date.now();
|
||||
const dataTime = data.timestamp.getTime();
|
||||
return (now - dataTime) < 60000; // Data is recent if less than 1 minute old
|
||||
}
|
||||
|
||||
private addToCache(symbol: string, data: MarketData): void {
|
||||
const key = `${symbol}_live`;
|
||||
if (!this.cache.has(key)) {
|
||||
this.cache.set(key, []);
|
||||
}
|
||||
|
||||
const cached = this.cache.get(key)!;
|
||||
cached.push(data);
|
||||
|
||||
// Keep only last 1000 data points for live data
|
||||
if (cached.length > 1000) {
|
||||
cached.splice(0, cached.length - 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private trimCache(): void {
|
||||
if (this.cache.size > this.config.cacheSize!) {
|
||||
// Remove oldest entries
|
||||
const entries = Array.from(this.cache.entries());
|
||||
const toRemove = entries.slice(0, entries.length - this.config.cacheSize!);
|
||||
toRemove.forEach(([key]) => this.cache.delete(key));
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.questdb) {
|
||||
await this.questdb.close();
|
||||
}
|
||||
this.cache.clear();
|
||||
this.liveSubscriptions.clear();
|
||||
logger.info('Unified data provider closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function
|
||||
export function createUnifiedDataProvider(eventBus: EventBus, config?: DataProviderConfig): UnifiedDataProvider {
|
||||
return new UnifiedDataProvider(eventBus, config);
|
||||
}
|
||||
37
apps/execution-service/package.json
Normal file
37
apps/execution-service/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "@stock-bot/execution-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Execution service for stock trading bot - handles order execution and broker integration",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"start": "bun src/index.ts",
|
||||
"test": "bun test",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.12.0",
|
||||
"hono": "^4.6.1",
|
||||
"@stock-bot/config": "*",
|
||||
"@stock-bot/logger": "*",
|
||||
"@stock-bot/types": "*",
|
||||
"@stock-bot/event-bus": "*",
|
||||
"@stock-bot/utils": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"keywords": [
|
||||
"trading",
|
||||
"execution",
|
||||
"broker",
|
||||
"orders",
|
||||
"stock-bot"
|
||||
],
|
||||
"author": "Stock Bot Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
94
apps/execution-service/src/broker/interface.ts
Normal file
94
apps/execution-service/src/broker/interface.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Order, OrderResult, OrderStatus } from '@stock-bot/types';
|
||||
|
||||
export interface BrokerInterface {
|
||||
/**
|
||||
* Execute an order with the broker
|
||||
*/
|
||||
executeOrder(order: Order): Promise<OrderResult>;
|
||||
|
||||
/**
|
||||
* Get order status from broker
|
||||
*/
|
||||
getOrderStatus(orderId: string): Promise<OrderStatus>;
|
||||
|
||||
/**
|
||||
* Cancel an order
|
||||
*/
|
||||
cancelOrder(orderId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get current positions
|
||||
*/
|
||||
getPositions(): Promise<Position[]>;
|
||||
|
||||
/**
|
||||
* Get account balance
|
||||
*/
|
||||
getAccountBalance(): Promise<AccountBalance>;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
unrealizedPnL: number;
|
||||
side: 'long' | 'short';
|
||||
}
|
||||
|
||||
export interface AccountBalance {
|
||||
totalValue: number;
|
||||
availableCash: number;
|
||||
buyingPower: number;
|
||||
marginUsed: number;
|
||||
}
|
||||
|
||||
export class MockBroker implements BrokerInterface {
|
||||
private orders: Map<string, OrderResult> = new Map();
|
||||
private positions: Position[] = [];
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
const orderId = `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const result: OrderResult = {
|
||||
orderId,
|
||||
symbol: order.symbol,
|
||||
quantity: order.quantity,
|
||||
side: order.side,
|
||||
status: 'filled',
|
||||
executedPrice: order.price || 100, // Mock price
|
||||
executedAt: new Date(),
|
||||
commission: 1.0
|
||||
};
|
||||
|
||||
this.orders.set(orderId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getOrderStatus(orderId: string): Promise<OrderStatus> {
|
||||
const order = this.orders.get(orderId);
|
||||
return order?.status || 'unknown';
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: string): Promise<boolean> {
|
||||
const order = this.orders.get(orderId);
|
||||
if (order && order.status === 'pending') {
|
||||
order.status = 'cancelled';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getPositions(): Promise<Position[]> {
|
||||
return this.positions;
|
||||
}
|
||||
|
||||
async getAccountBalance(): Promise<AccountBalance> {
|
||||
return {
|
||||
totalValue: 100000,
|
||||
availableCash: 50000,
|
||||
buyingPower: 200000,
|
||||
marginUsed: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
57
apps/execution-service/src/execution/order-manager.ts
Normal file
57
apps/execution-service/src/execution/order-manager.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Order, OrderResult } from '@stock-bot/types';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import { BrokerInterface } from '../broker/interface.ts';
|
||||
|
||||
export class OrderManager {
|
||||
private broker: BrokerInterface;
|
||||
private pendingOrders: Map<string, Order> = new Map();
|
||||
|
||||
constructor(broker: BrokerInterface) {
|
||||
this.broker = broker;
|
||||
}
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
try {
|
||||
logger.info(`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`);
|
||||
|
||||
// Add to pending orders
|
||||
const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.pendingOrders.set(orderId, order);
|
||||
|
||||
// Execute with broker
|
||||
const result = await this.broker.executeOrder(order);
|
||||
|
||||
// Remove from pending
|
||||
this.pendingOrders.delete(orderId);
|
||||
|
||||
logger.info(`Order executed successfully: ${result.orderId}`);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Order execution failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: string): Promise<boolean> {
|
||||
try {
|
||||
const success = await this.broker.cancelOrder(orderId);
|
||||
if (success) {
|
||||
this.pendingOrders.delete(orderId);
|
||||
logger.info(`Order cancelled: ${orderId}`);
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('Order cancellation failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrderStatus(orderId: string) {
|
||||
return await this.broker.getOrderStatus(orderId);
|
||||
}
|
||||
|
||||
getPendingOrders(): Order[] {
|
||||
return Array.from(this.pendingOrders.values());
|
||||
}
|
||||
}
|
||||
111
apps/execution-service/src/execution/risk-manager.ts
Normal file
111
apps/execution-service/src/execution/risk-manager.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { Order } from '@stock-bot/types';
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
|
||||
export interface RiskRule {
|
||||
name: string;
|
||||
validate(order: Order, context: RiskContext): Promise<RiskValidationResult>;
|
||||
}
|
||||
|
||||
export interface RiskContext {
|
||||
currentPositions: Map<string, number>;
|
||||
accountBalance: number;
|
||||
totalExposure: number;
|
||||
maxPositionSize: number;
|
||||
maxDailyLoss: number;
|
||||
}
|
||||
|
||||
export interface RiskValidationResult {
|
||||
isValid: boolean;
|
||||
reason?: string;
|
||||
severity: 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
export class RiskManager {
|
||||
private logger = createLogger('risk-manager');
|
||||
private rules: RiskRule[] = [];
|
||||
|
||||
constructor() {
|
||||
this.initializeDefaultRules();
|
||||
}
|
||||
|
||||
addRule(rule: RiskRule): void {
|
||||
this.rules.push(rule);
|
||||
}
|
||||
|
||||
async validateOrder(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
for (const rule of this.rules) {
|
||||
const result = await rule.validate(order, context);
|
||||
if (!result.isValid) {
|
||||
logger.warn(`Risk rule violation: ${rule.name}`, {
|
||||
order,
|
||||
reason: result.reason
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
}
|
||||
|
||||
private initializeDefaultRules(): void {
|
||||
// Position size rule
|
||||
this.addRule({
|
||||
name: 'MaxPositionSize',
|
||||
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
const orderValue = order.quantity * (order.price || 0);
|
||||
|
||||
if (orderValue > context.maxPositionSize) {
|
||||
return {
|
||||
isValid: false,
|
||||
reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`,
|
||||
severity: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
}
|
||||
});
|
||||
|
||||
// Balance check rule
|
||||
this.addRule({
|
||||
name: 'SufficientBalance',
|
||||
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
const orderValue = order.quantity * (order.price || 0);
|
||||
|
||||
if (order.side === 'buy' && orderValue > context.accountBalance) {
|
||||
return {
|
||||
isValid: false,
|
||||
reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`,
|
||||
severity: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
}
|
||||
});
|
||||
|
||||
// Concentration risk rule
|
||||
this.addRule({
|
||||
name: 'ConcentrationLimit',
|
||||
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
const currentPosition = context.currentPositions.get(order.symbol) || 0;
|
||||
const newPosition = order.side === 'buy' ?
|
||||
currentPosition + order.quantity :
|
||||
currentPosition - order.quantity;
|
||||
|
||||
const positionValue = Math.abs(newPosition) * (order.price || 0);
|
||||
const concentrationRatio = positionValue / context.accountBalance;
|
||||
|
||||
if (concentrationRatio > 0.25) { // 25% max concentration
|
||||
return {
|
||||
isValid: false,
|
||||
reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`,
|
||||
severity: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
97
apps/execution-service/src/index.ts
Normal file
97
apps/execution-service/src/index.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { Hono } from 'hono';
|
||||
import { serve } from '@hono/node-server';
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { config } from '@stock-bot/config';
|
||||
// import { BrokerInterface } from './broker/interface.ts';
|
||||
// import { OrderManager } from './execution/order-manager.ts';
|
||||
// import { RiskManager } from './execution/risk-manager.ts';
|
||||
|
||||
const app = new Hono();
|
||||
const logger = createLogger('execution-service');
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
status: 'healthy',
|
||||
service: 'execution-service',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Order execution endpoints
|
||||
app.post('/orders/execute', async (c) => {
|
||||
try {
|
||||
const orderRequest = await c.req.json();
|
||||
logger.info('Received order execution request', orderRequest);
|
||||
|
||||
// TODO: Validate order and execute
|
||||
return c.json({
|
||||
orderId: `order_${Date.now()}`,
|
||||
status: 'pending',
|
||||
message: 'Order submitted for execution'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Order execution failed', error);
|
||||
return c.json({ error: 'Order execution failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/orders/:orderId/status', async (c) => {
|
||||
const orderId = c.req.param('orderId');
|
||||
|
||||
try {
|
||||
// TODO: Get order status from broker
|
||||
return c.json({
|
||||
orderId,
|
||||
status: 'filled',
|
||||
executedAt: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get order status', error);
|
||||
return c.json({ error: 'Failed to get order status' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/orders/:orderId/cancel', async (c) => {
|
||||
const orderId = c.req.param('orderId');
|
||||
|
||||
try {
|
||||
// TODO: Cancel order with broker
|
||||
return c.json({
|
||||
orderId,
|
||||
status: 'cancelled',
|
||||
cancelledAt: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to cancel order', error);
|
||||
return c.json({ error: 'Failed to cancel order' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Risk management endpoints
|
||||
app.get('/risk/position/:symbol', async (c) => {
|
||||
const symbol = c.req.param('symbol');
|
||||
|
||||
try {
|
||||
// TODO: Get position risk metrics
|
||||
return c.json({
|
||||
symbol,
|
||||
position: 100,
|
||||
exposure: 10000,
|
||||
risk: 'low'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get position risk', error);
|
||||
return c.json({ error: 'Failed to get position risk' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const port = config.EXECUTION_SERVICE_PORT || 3004;
|
||||
|
||||
logger.info(`Starting execution service on port ${port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port
|
||||
}, (info) => {
|
||||
logger.info(`Execution service is running on port ${info.port}`);
|
||||
});
|
||||
38
apps/portfolio-service/package.json
Normal file
38
apps/portfolio-service/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "@stock-bot/portfolio-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Portfolio service for stock trading bot - handles portfolio tracking and performance analytics",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"start": "bun src/index.ts",
|
||||
"test": "bun test",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.12.0",
|
||||
"hono": "^4.6.1",
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/types": "workspace:*",
|
||||
"@stock-bot/questdb-client": "workspace:*",
|
||||
"@stock-bot/utils": "workspace:*",
|
||||
"@stock-bot/data-frame": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"keywords": [
|
||||
"trading",
|
||||
"portfolio",
|
||||
"performance",
|
||||
"analytics",
|
||||
"stock-bot"
|
||||
],
|
||||
"author": "Stock Bot Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
204
apps/portfolio-service/src/analytics/performance-analyzer.ts
Normal file
204
apps/portfolio-service/src/analytics/performance-analyzer.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { PortfolioSnapshot, Trade } from '../portfolio/portfolio-manager.ts';
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
totalReturn: number;
|
||||
annualizedReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
volatility: number;
|
||||
beta: number;
|
||||
alpha: number;
|
||||
calmarRatio: number;
|
||||
sortinoRatio: number;
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
var95: number; // Value at Risk (95% confidence)
|
||||
cvar95: number; // Conditional Value at Risk
|
||||
maxDrawdown: number;
|
||||
downsideDeviation: number;
|
||||
correlationMatrix: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
export class PerformanceAnalyzer {
|
||||
private snapshots: PortfolioSnapshot[] = [];
|
||||
private benchmarkReturns: number[] = []; // S&P 500 or other benchmark
|
||||
|
||||
addSnapshot(snapshot: PortfolioSnapshot): void {
|
||||
this.snapshots.push(snapshot);
|
||||
// Keep only last 252 trading days (1 year)
|
||||
if (this.snapshots.length > 252) {
|
||||
this.snapshots = this.snapshots.slice(-252);
|
||||
}
|
||||
}
|
||||
|
||||
calculatePerformanceMetrics(period: 'daily' | 'weekly' | 'monthly' = 'daily'): PerformanceMetrics {
|
||||
if (this.snapshots.length < 2) {
|
||||
throw new Error('Need at least 2 snapshots to calculate performance');
|
||||
}
|
||||
|
||||
const returns = this.calculateReturns(period);
|
||||
const riskFreeRate = 0.02; // 2% annual risk-free rate
|
||||
|
||||
return {
|
||||
totalReturn: this.calculateTotalReturn(),
|
||||
annualizedReturn: this.calculateAnnualizedReturn(returns),
|
||||
sharpeRatio: this.calculateSharpeRatio(returns, riskFreeRate),
|
||||
maxDrawdown: this.calculateMaxDrawdown(),
|
||||
volatility: this.calculateVolatility(returns),
|
||||
beta: this.calculateBeta(returns),
|
||||
alpha: this.calculateAlpha(returns, riskFreeRate),
|
||||
calmarRatio: this.calculateCalmarRatio(returns),
|
||||
sortinoRatio: this.calculateSortinoRatio(returns, riskFreeRate)
|
||||
};
|
||||
}
|
||||
|
||||
calculateRiskMetrics(): RiskMetrics {
|
||||
const returns = this.calculateReturns('daily');
|
||||
|
||||
return {
|
||||
var95: this.calculateVaR(returns, 0.95),
|
||||
cvar95: this.calculateCVaR(returns, 0.95),
|
||||
maxDrawdown: this.calculateMaxDrawdown(),
|
||||
downsideDeviation: this.calculateDownsideDeviation(returns),
|
||||
correlationMatrix: {} // TODO: Implement correlation matrix
|
||||
};
|
||||
}
|
||||
|
||||
private calculateReturns(period: 'daily' | 'weekly' | 'monthly'): number[] {
|
||||
if (this.snapshots.length < 2) return [];
|
||||
|
||||
const returns: number[] = [];
|
||||
|
||||
for (let i = 1; i < this.snapshots.length; i++) {
|
||||
const currentValue = this.snapshots[i].totalValue;
|
||||
const previousValue = this.snapshots[i - 1].totalValue;
|
||||
const return_ = (currentValue - previousValue) / previousValue;
|
||||
returns.push(return_);
|
||||
}
|
||||
|
||||
return returns;
|
||||
}
|
||||
|
||||
private calculateTotalReturn(): number {
|
||||
if (this.snapshots.length < 2) return 0;
|
||||
|
||||
const firstValue = this.snapshots[0].totalValue;
|
||||
const lastValue = this.snapshots[this.snapshots.length - 1].totalValue;
|
||||
|
||||
return (lastValue - firstValue) / firstValue;
|
||||
}
|
||||
|
||||
private calculateAnnualizedReturn(returns: number[]): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
return Math.pow(1 + avgReturn, 252) - 1; // 252 trading days per year
|
||||
}
|
||||
|
||||
private calculateVolatility(returns: number[]): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length;
|
||||
|
||||
return Math.sqrt(variance * 252); // Annualized volatility
|
||||
}
|
||||
|
||||
private calculateSharpeRatio(returns: number[], riskFreeRate: number): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const annualizedReturn = Math.pow(1 + avgReturn, 252) - 1;
|
||||
const volatility = this.calculateVolatility(returns);
|
||||
|
||||
if (volatility === 0) return 0;
|
||||
|
||||
return (annualizedReturn - riskFreeRate) / volatility;
|
||||
}
|
||||
|
||||
private calculateMaxDrawdown(): number {
|
||||
if (this.snapshots.length === 0) return 0;
|
||||
|
||||
let maxDrawdown = 0;
|
||||
let peak = this.snapshots[0].totalValue;
|
||||
|
||||
for (const snapshot of this.snapshots) {
|
||||
if (snapshot.totalValue > peak) {
|
||||
peak = snapshot.totalValue;
|
||||
}
|
||||
|
||||
const drawdown = (peak - snapshot.totalValue) / peak;
|
||||
maxDrawdown = Math.max(maxDrawdown, drawdown);
|
||||
}
|
||||
|
||||
return maxDrawdown;
|
||||
}
|
||||
|
||||
private calculateBeta(returns: number[]): number {
|
||||
if (returns.length === 0 || this.benchmarkReturns.length === 0) return 1.0;
|
||||
|
||||
// Simple beta calculation - would need actual benchmark data
|
||||
return 1.0; // Placeholder
|
||||
}
|
||||
|
||||
private calculateAlpha(returns: number[], riskFreeRate: number): number {
|
||||
const beta = this.calculateBeta(returns);
|
||||
const portfolioReturn = this.calculateAnnualizedReturn(returns);
|
||||
const benchmarkReturn = 0.10; // 10% benchmark return (placeholder)
|
||||
|
||||
return portfolioReturn - (riskFreeRate + beta * (benchmarkReturn - riskFreeRate));
|
||||
}
|
||||
|
||||
private calculateCalmarRatio(returns: number[]): number {
|
||||
const annualizedReturn = this.calculateAnnualizedReturn(returns);
|
||||
const maxDrawdown = this.calculateMaxDrawdown();
|
||||
|
||||
if (maxDrawdown === 0) return 0;
|
||||
|
||||
return annualizedReturn / maxDrawdown;
|
||||
}
|
||||
|
||||
private calculateSortinoRatio(returns: number[], riskFreeRate: number): number {
|
||||
const annualizedReturn = this.calculateAnnualizedReturn(returns);
|
||||
const downsideDeviation = this.calculateDownsideDeviation(returns);
|
||||
|
||||
if (downsideDeviation === 0) return 0;
|
||||
|
||||
return (annualizedReturn - riskFreeRate) / downsideDeviation;
|
||||
}
|
||||
|
||||
private calculateDownsideDeviation(returns: number[]): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const negativeReturns = returns.filter(ret => ret < 0);
|
||||
if (negativeReturns.length === 0) return 0;
|
||||
|
||||
const avgNegativeReturn = negativeReturns.reduce((sum, ret) => sum + ret, 0) / negativeReturns.length;
|
||||
const variance = negativeReturns.reduce((sum, ret) => sum + Math.pow(ret - avgNegativeReturn, 2), 0) / negativeReturns.length;
|
||||
|
||||
return Math.sqrt(variance * 252); // Annualized
|
||||
}
|
||||
|
||||
private calculateVaR(returns: number[], confidence: number): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = returns.slice().sort((a, b) => a - b);
|
||||
const index = Math.floor((1 - confidence) * sortedReturns.length);
|
||||
|
||||
return -sortedReturns[index]; // Return as positive value
|
||||
}
|
||||
|
||||
private calculateCVaR(returns: number[], confidence: number): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = returns.slice().sort((a, b) => a - b);
|
||||
const cutoffIndex = Math.floor((1 - confidence) * sortedReturns.length);
|
||||
const tailReturns = sortedReturns.slice(0, cutoffIndex + 1);
|
||||
|
||||
if (tailReturns.length === 0) return 0;
|
||||
|
||||
const avgTailReturn = tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
|
||||
return -avgTailReturn; // Return as positive value
|
||||
}
|
||||
}
|
||||
133
apps/portfolio-service/src/index.ts
Normal file
133
apps/portfolio-service/src/index.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { Hono } from 'hono';
|
||||
import { serve } from '@hono/node-server';
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { config } from '@stock-bot/config';
|
||||
import { PortfolioManager } from './portfolio/portfolio-manager.ts';
|
||||
import { PerformanceAnalyzer } from './analytics/performance-analyzer.ts';
|
||||
|
||||
const app = new Hono();
|
||||
const logger = createLogger('portfolio-service');
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
status: 'healthy',
|
||||
service: 'portfolio-service',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Portfolio endpoints
|
||||
app.get('/portfolio/overview', async (c) => {
|
||||
try {
|
||||
// TODO: Get portfolio overview
|
||||
return c.json({
|
||||
totalValue: 125000,
|
||||
totalReturn: 25000,
|
||||
totalReturnPercent: 25.0,
|
||||
dayChange: 1250,
|
||||
dayChangePercent: 1.0,
|
||||
positions: []
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get portfolio overview', error);
|
||||
return c.json({ error: 'Failed to get portfolio overview' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/portfolio/positions', async (c) => {
|
||||
try {
|
||||
// TODO: Get current positions
|
||||
return c.json([
|
||||
{
|
||||
symbol: 'AAPL',
|
||||
quantity: 100,
|
||||
averagePrice: 150.0,
|
||||
currentPrice: 155.0,
|
||||
marketValue: 15500,
|
||||
unrealizedPnL: 500,
|
||||
unrealizedPnLPercent: 3.33
|
||||
}
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get positions', error);
|
||||
return c.json({ error: 'Failed to get positions' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/portfolio/history', async (c) => {
|
||||
const days = c.req.query('days') || '30';
|
||||
|
||||
try {
|
||||
// TODO: Get portfolio history
|
||||
return c.json({
|
||||
period: `${days} days`,
|
||||
data: []
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get portfolio history', error);
|
||||
return c.json({ error: 'Failed to get portfolio history' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Performance analytics endpoints
|
||||
app.get('/analytics/performance', async (c) => {
|
||||
const period = c.req.query('period') || '1M';
|
||||
|
||||
try {
|
||||
// TODO: Calculate performance metrics
|
||||
return c.json({
|
||||
period,
|
||||
totalReturn: 0.25,
|
||||
annualizedReturn: 0.30,
|
||||
sharpeRatio: 1.5,
|
||||
maxDrawdown: 0.05,
|
||||
volatility: 0.15,
|
||||
beta: 1.1,
|
||||
alpha: 0.02
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get performance analytics', error);
|
||||
return c.json({ error: 'Failed to get performance analytics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/analytics/risk', async (c) => {
|
||||
try {
|
||||
// TODO: Calculate risk metrics
|
||||
return c.json({
|
||||
var95: 0.02,
|
||||
cvar95: 0.03,
|
||||
maxDrawdown: 0.05,
|
||||
downside_deviation: 0.08,
|
||||
correlation_matrix: {}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get risk analytics', error);
|
||||
return c.json({ error: 'Failed to get risk analytics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/analytics/attribution', async (c) => {
|
||||
try {
|
||||
// TODO: Calculate performance attribution
|
||||
return c.json({
|
||||
sector_allocation: {},
|
||||
security_selection: {},
|
||||
interaction_effect: {}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get attribution analytics', error);
|
||||
return c.json({ error: 'Failed to get attribution analytics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const port = config.PORTFOLIO_SERVICE_PORT || 3005;
|
||||
|
||||
logger.info(`Starting portfolio service on port ${port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port
|
||||
}, (info) => {
|
||||
logger.info(`Portfolio service is running on port ${info.port}`);
|
||||
});
|
||||
159
apps/portfolio-service/src/portfolio/portfolio-manager.ts
Normal file
159
apps/portfolio-service/src/portfolio/portfolio-manager.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { createLogger } from '@stock-bot/logger';
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
marketValue: number;
|
||||
unrealizedPnL: number;
|
||||
unrealizedPnLPercent: number;
|
||||
costBasis: number;
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
export interface PortfolioSnapshot {
|
||||
timestamp: Date;
|
||||
totalValue: number;
|
||||
cashBalance: number;
|
||||
positions: Position[];
|
||||
totalReturn: number;
|
||||
totalReturnPercent: number;
|
||||
dayChange: number;
|
||||
dayChangePercent: number;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id: string;
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
side: 'buy' | 'sell';
|
||||
timestamp: Date;
|
||||
commission: number;
|
||||
}
|
||||
|
||||
export class PortfolioManager {
|
||||
private logger = createLogger('PortfolioManager');
|
||||
private positions: Map<string, Position> = new Map();
|
||||
private trades: Trade[] = [];
|
||||
private cashBalance: number = 100000; // Starting cash
|
||||
|
||||
constructor(initialCash: number = 100000) {
|
||||
this.cashBalance = initialCash;
|
||||
}
|
||||
|
||||
addTrade(trade: Trade): void {
|
||||
this.trades.push(trade);
|
||||
this.updatePosition(trade);
|
||||
logger.info(`Trade added: ${trade.symbol} ${trade.side} ${trade.quantity} @ ${trade.price}`);
|
||||
}
|
||||
|
||||
private updatePosition(trade: Trade): void {
|
||||
const existing = this.positions.get(trade.symbol);
|
||||
|
||||
if (!existing) {
|
||||
// New position
|
||||
if (trade.side === 'buy') {
|
||||
this.positions.set(trade.symbol, {
|
||||
symbol: trade.symbol,
|
||||
quantity: trade.quantity,
|
||||
averagePrice: trade.price,
|
||||
currentPrice: trade.price,
|
||||
marketValue: trade.quantity * trade.price,
|
||||
unrealizedPnL: 0,
|
||||
unrealizedPnLPercent: 0,
|
||||
costBasis: trade.quantity * trade.price + trade.commission,
|
||||
lastUpdated: trade.timestamp
|
||||
});
|
||||
this.cashBalance -= (trade.quantity * trade.price + trade.commission);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing position
|
||||
if (trade.side === 'buy') {
|
||||
const newQuantity = existing.quantity + trade.quantity;
|
||||
const newCostBasis = existing.costBasis + (trade.quantity * trade.price) + trade.commission;
|
||||
|
||||
existing.quantity = newQuantity;
|
||||
existing.averagePrice = (newCostBasis - this.getTotalCommissions(trade.symbol)) / newQuantity;
|
||||
existing.costBasis = newCostBasis;
|
||||
existing.lastUpdated = trade.timestamp;
|
||||
|
||||
this.cashBalance -= (trade.quantity * trade.price + trade.commission);
|
||||
|
||||
} else if (trade.side === 'sell') {
|
||||
existing.quantity -= trade.quantity;
|
||||
existing.lastUpdated = trade.timestamp;
|
||||
|
||||
const proceeds = trade.quantity * trade.price - trade.commission;
|
||||
this.cashBalance += proceeds;
|
||||
|
||||
// Remove position if quantity is zero
|
||||
if (existing.quantity <= 0) {
|
||||
this.positions.delete(trade.symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePrice(symbol: string, price: number): void {
|
||||
const position = this.positions.get(symbol);
|
||||
if (position) {
|
||||
position.currentPrice = price;
|
||||
position.marketValue = position.quantity * price;
|
||||
position.unrealizedPnL = position.marketValue - (position.quantity * position.averagePrice);
|
||||
position.unrealizedPnLPercent = position.unrealizedPnL / (position.quantity * position.averagePrice) * 100;
|
||||
position.lastUpdated = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
getPosition(symbol: string): Position | undefined {
|
||||
return this.positions.get(symbol);
|
||||
}
|
||||
|
||||
getAllPositions(): Position[] {
|
||||
return Array.from(this.positions.values());
|
||||
}
|
||||
|
||||
getPortfolioSnapshot(): PortfolioSnapshot {
|
||||
const positions = this.getAllPositions();
|
||||
const totalMarketValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0);
|
||||
const totalValue = totalMarketValue + this.cashBalance;
|
||||
const totalUnrealizedPnL = positions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0);
|
||||
|
||||
return {
|
||||
timestamp: new Date(),
|
||||
totalValue,
|
||||
cashBalance: this.cashBalance,
|
||||
positions,
|
||||
totalReturn: totalUnrealizedPnL, // Simplified - should include realized gains
|
||||
totalReturnPercent: (totalUnrealizedPnL / (totalValue - totalUnrealizedPnL)) * 100,
|
||||
dayChange: 0, // TODO: Calculate from previous day
|
||||
dayChangePercent: 0
|
||||
};
|
||||
}
|
||||
|
||||
getTrades(symbol?: string): Trade[] {
|
||||
if (symbol) {
|
||||
return this.trades.filter(trade => trade.symbol === symbol);
|
||||
}
|
||||
return this.trades;
|
||||
}
|
||||
|
||||
private getTotalCommissions(symbol: string): number {
|
||||
return this.trades
|
||||
.filter(trade => trade.symbol === symbol)
|
||||
.reduce((sum, trade) => sum + trade.commission, 0);
|
||||
}
|
||||
|
||||
getCashBalance(): number {
|
||||
return this.cashBalance;
|
||||
}
|
||||
|
||||
getNetLiquidationValue(): number {
|
||||
const positions = this.getAllPositions();
|
||||
const positionValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0);
|
||||
return positionValue + this.cashBalance;
|
||||
}
|
||||
}
|
||||
26
apps/processing-service/package.json
Normal file
26
apps/processing-service/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@stock-bot/processing-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Combined data processing and technical indicators service",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target node",
|
||||
"start": "bun dist/index.js",
|
||||
"test": "bun test",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/types": "workspace:*",
|
||||
"@stock-bot/utils": "workspace:*",
|
||||
"@stock-bot/event-bus": "workspace:*",
|
||||
"@stock-bot/vector-engine": "workspace:*",
|
||||
"hono": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
54
apps/processing-service/src/index.ts
Normal file
54
apps/processing-service/src/index.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Processing Service - Technical indicators and data processing
|
||||
*/
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { Hono } from 'hono';
|
||||
import { serve } from '@hono/node-server';
|
||||
|
||||
// Load environment variables
|
||||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
const logger = createLogger('processing-service');
|
||||
const PORT = parseInt(process.env.PROCESSING_SERVICE_PORT || '3003');
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
service: 'processing-service',
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Technical indicators endpoint
|
||||
app.post('/api/indicators', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Technical indicators request', { indicators: body.indicators });
|
||||
|
||||
// TODO: Implement technical indicators processing
|
||||
return c.json({
|
||||
message: 'Technical indicators endpoint - not implemented yet',
|
||||
requestedIndicators: body.indicators
|
||||
});
|
||||
});
|
||||
|
||||
// Vectorized processing endpoint
|
||||
app.post('/api/vectorized/process', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Vectorized processing request', { dataPoints: body.data?.length });
|
||||
|
||||
// TODO: Implement vectorized processing
|
||||
return c.json({
|
||||
message: 'Vectorized processing endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: PORT,
|
||||
});
|
||||
|
||||
logger.info(`Processing Service started on port ${PORT}`);
|
||||
82
apps/processing-service/src/indicators/indicators.ts
Normal file
82
apps/processing-service/src/indicators/indicators.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Technical Indicators Service
|
||||
* Leverages @stock-bot/utils for calculations
|
||||
*/
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import {
|
||||
sma,
|
||||
ema,
|
||||
rsi,
|
||||
macd
|
||||
} from '@stock-bot/utils';
|
||||
|
||||
const logger = createLogger('indicators-service');
|
||||
|
||||
export interface IndicatorRequest {
|
||||
symbol: string;
|
||||
data: number[];
|
||||
indicators: string[];
|
||||
parameters?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IndicatorResult {
|
||||
symbol: string;
|
||||
timestamp: Date;
|
||||
indicators: Record<string, number[]>;
|
||||
}
|
||||
|
||||
export class IndicatorsService {
|
||||
async calculateIndicators(request: IndicatorRequest): Promise<IndicatorResult> {
|
||||
logger.info('Calculating indicators', {
|
||||
symbol: request.symbol,
|
||||
indicators: request.indicators,
|
||||
dataPoints: request.data.length
|
||||
});
|
||||
|
||||
const results: Record<string, number[]> = {};
|
||||
|
||||
for (const indicator of request.indicators) {
|
||||
try {
|
||||
switch (indicator.toLowerCase()) {
|
||||
case 'sma':
|
||||
const smaPeriod = request.parameters?.smaPeriod || 20;
|
||||
results.sma = sma(request.data, smaPeriod);
|
||||
break;
|
||||
|
||||
case 'ema':
|
||||
const emaPeriod = request.parameters?.emaPeriod || 20;
|
||||
results.ema = ema(request.data, emaPeriod);
|
||||
break;
|
||||
|
||||
case 'rsi':
|
||||
const rsiPeriod = request.parameters?.rsiPeriod || 14;
|
||||
results.rsi = rsi(request.data, rsiPeriod);
|
||||
break;
|
||||
|
||||
case 'macd':
|
||||
const fast = request.parameters?.macdFast || 12;
|
||||
const slow = request.parameters?.macdSlow || 26;
|
||||
const signal = request.parameters?.macdSignal || 9;
|
||||
results.macd = macd(request.data, fast, slow, signal).macd;
|
||||
break;
|
||||
|
||||
case 'stochastic':
|
||||
// TODO: Implement stochastic oscillator
|
||||
logger.warn('Stochastic oscillator not implemented yet');
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn('Unknown indicator requested', { indicator });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error calculating indicator', { indicator, error });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
symbol: request.symbol,
|
||||
timestamp: new Date(),
|
||||
indicators: results
|
||||
};
|
||||
}
|
||||
}
|
||||
33
apps/strategy-service/package.json
Normal file
33
apps/strategy-service/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@stock-bot/strategy-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Combined strategy execution and multi-mode backtesting service",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target node",
|
||||
"start": "bun dist/index.js",
|
||||
"test": "bun test", "clean": "rm -rf dist",
|
||||
"backtest": "bun src/cli/index.ts",
|
||||
"optimize": "bun src/cli/index.ts optimize",
|
||||
"cli": "bun src/cli/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/types": "workspace:*",
|
||||
"@stock-bot/utils": "workspace:*",
|
||||
"@stock-bot/event-bus": "workspace:*",
|
||||
"@stock-bot/strategy-engine": "workspace:*",
|
||||
"@stock-bot/vector-engine": "workspace:*",
|
||||
"@stock-bot/data-frame": "workspace:*",
|
||||
"@stock-bot/questdb-client": "workspace:*",
|
||||
"hono": "^4.0.0",
|
||||
"commander": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
75
apps/strategy-service/src/backtesting/modes/event-mode.ts
Normal file
75
apps/strategy-service/src/backtesting/modes/event-mode.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Event-Driven Backtesting Mode
|
||||
* Processes data point by point with realistic order execution
|
||||
*/
|
||||
import { ExecutionMode, Order, OrderResult, MarketData } from '../../framework/execution-mode';
|
||||
|
||||
export interface BacktestConfig {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
initialCapital: number;
|
||||
slippageModel?: string;
|
||||
commissionModel?: string;
|
||||
}
|
||||
|
||||
export class EventBacktestMode extends ExecutionMode {
|
||||
name = 'event-driven';
|
||||
private simulationTime: Date;
|
||||
private historicalData: Map<string, MarketData[]> = new Map();
|
||||
|
||||
constructor(private config: BacktestConfig) {
|
||||
super();
|
||||
this.simulationTime = config.startDate;
|
||||
}
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
this.logger.debug('Simulating order execution', {
|
||||
orderId: order.id,
|
||||
simulationTime: this.simulationTime
|
||||
});
|
||||
|
||||
// TODO: Implement realistic order simulation
|
||||
// Include slippage, commission, market impact
|
||||
const simulatedResult: OrderResult = {
|
||||
orderId: order.id,
|
||||
symbol: order.symbol,
|
||||
executedQuantity: order.quantity,
|
||||
executedPrice: 100, // TODO: Get realistic price
|
||||
commission: 1.0, // TODO: Calculate based on commission model
|
||||
slippage: 0.01, // TODO: Calculate based on slippage model
|
||||
timestamp: this.simulationTime,
|
||||
executionTime: 50 // ms
|
||||
};
|
||||
|
||||
return simulatedResult;
|
||||
}
|
||||
|
||||
getCurrentTime(): Date {
|
||||
return this.simulationTime;
|
||||
}
|
||||
|
||||
async getMarketData(symbol: string): Promise<MarketData> {
|
||||
const data = this.historicalData.get(symbol) || [];
|
||||
const currentData = data.find(d => d.timestamp <= this.simulationTime);
|
||||
|
||||
if (!currentData) {
|
||||
throw new Error(`No market data available for ${symbol} at ${this.simulationTime}`);
|
||||
}
|
||||
|
||||
return currentData;
|
||||
}
|
||||
|
||||
async publishEvent(event: string, data: any): Promise<void> {
|
||||
// In-memory event bus for simulation
|
||||
this.logger.debug('Publishing simulation event', { event, data });
|
||||
}
|
||||
|
||||
// Simulation control methods
|
||||
advanceTime(newTime: Date): void {
|
||||
this.simulationTime = newTime;
|
||||
}
|
||||
|
||||
loadHistoricalData(symbol: string, data: MarketData[]): void {
|
||||
this.historicalData.set(symbol, data);
|
||||
}
|
||||
}
|
||||
422
apps/strategy-service/src/backtesting/modes/hybrid-mode.ts
Normal file
422
apps/strategy-service/src/backtesting/modes/hybrid-mode.ts
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
import { createLogger } from '@stock-bot/logger';
|
||||
import { EventBus } from '@stock-bot/event-bus';
|
||||
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
|
||||
import { DataFrame } from '@stock-bot/data-frame';
|
||||
import { ExecutionMode, BacktestContext, BacktestResult } from '../framework/execution-mode';
|
||||
import EventMode from './event-mode';
|
||||
import VectorizedMode from './vectorized-mode';
|
||||
import { create } from 'domain';
|
||||
|
||||
export interface HybridModeConfig {
|
||||
vectorizedThreshold: number; // Switch to vectorized if data points > threshold
|
||||
warmupPeriod: number; // Number of periods for initial vectorized calculation
|
||||
eventDrivenRealtime: boolean; // Use event-driven for real-time portions
|
||||
optimizeIndicators: boolean; // Pre-calculate indicators vectorized
|
||||
batchSize: number; // Size of batches for hybrid processing
|
||||
}
|
||||
|
||||
export class HybridMode extends ExecutionMode {
|
||||
private vectorEngine: VectorEngine;
|
||||
private eventMode: EventMode;
|
||||
private vectorizedMode: VectorizedMode;
|
||||
private config: HybridModeConfig;
|
||||
private precomputedIndicators: Map<string, number[]> = new Map();
|
||||
private currentIndex: number = 0;
|
||||
|
||||
constructor(
|
||||
context: BacktestContext,
|
||||
eventBus: EventBus,
|
||||
config: HybridModeConfig = {}
|
||||
) {
|
||||
super(context, eventBus);
|
||||
|
||||
this.config = {
|
||||
vectorizedThreshold: 50000,
|
||||
warmupPeriod: 1000,
|
||||
eventDrivenRealtime: true,
|
||||
optimizeIndicators: true,
|
||||
batchSize: 10000,
|
||||
...config
|
||||
};
|
||||
|
||||
this.vectorEngine = new VectorEngine();
|
||||
this.eventMode = new EventMode(context, eventBus);
|
||||
this.vectorizedMode = new VectorizedMode(context, eventBus);
|
||||
|
||||
this.logger = createLogger('hybrid-mode');
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
|
||||
// Initialize both modes
|
||||
await this.eventMode.initialize();
|
||||
await this.vectorizedMode.initialize();
|
||||
|
||||
this.logger.info('Hybrid mode initialized', {
|
||||
backtestId: this.context.backtestId,
|
||||
config: this.config
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<BacktestResult> {
|
||||
const startTime = Date.now();
|
||||
this.logger.info('Starting hybrid backtest execution');
|
||||
|
||||
try {
|
||||
// Determine execution strategy based on data size
|
||||
const dataSize = await this.estimateDataSize();
|
||||
|
||||
if (dataSize <= this.config.vectorizedThreshold) {
|
||||
// Small dataset: use pure vectorized approach
|
||||
this.logger.info('Using pure vectorized approach for small dataset', { dataSize });
|
||||
return await this.vectorizedMode.execute();
|
||||
}
|
||||
|
||||
// Large dataset: use hybrid approach
|
||||
this.logger.info('Using hybrid approach for large dataset', { dataSize });
|
||||
return await this.executeHybrid(startTime);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Hybrid backtest failed', {
|
||||
error,
|
||||
backtestId: this.context.backtestId
|
||||
});
|
||||
|
||||
await this.eventBus.publishBacktestUpdate(
|
||||
this.context.backtestId,
|
||||
0,
|
||||
{ status: 'failed', error: error.message }
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeHybrid(startTime: number): Promise<BacktestResult> {
|
||||
// Phase 1: Vectorized warmup and indicator pre-computation
|
||||
const warmupResult = await this.executeWarmupPhase();
|
||||
|
||||
// Phase 2: Event-driven processing with pre-computed indicators
|
||||
const eventResult = await this.executeEventPhase(warmupResult);
|
||||
|
||||
// Phase 3: Combine results
|
||||
const combinedResult = this.combineResults(warmupResult, eventResult, startTime);
|
||||
|
||||
await this.eventBus.publishBacktestUpdate(
|
||||
this.context.backtestId,
|
||||
100,
|
||||
{ status: 'completed', result: combinedResult }
|
||||
);
|
||||
|
||||
this.logger.info('Hybrid backtest completed', {
|
||||
backtestId: this.context.backtestId,
|
||||
duration: Date.now() - startTime,
|
||||
totalTrades: combinedResult.trades.length,
|
||||
warmupTrades: warmupResult.trades.length,
|
||||
eventTrades: eventResult.trades.length
|
||||
});
|
||||
|
||||
return combinedResult;
|
||||
}
|
||||
|
||||
private async executeWarmupPhase(): Promise<BacktestResult> {
|
||||
this.logger.info('Executing vectorized warmup phase', {
|
||||
warmupPeriod: this.config.warmupPeriod
|
||||
});
|
||||
|
||||
// Load warmup data
|
||||
const warmupData = await this.loadWarmupData();
|
||||
const dataFrame = this.createDataFrame(warmupData);
|
||||
|
||||
// Pre-compute indicators for entire dataset if optimization is enabled
|
||||
if (this.config.optimizeIndicators) {
|
||||
await this.precomputeIndicators(dataFrame);
|
||||
}
|
||||
|
||||
// Run vectorized backtest on warmup period
|
||||
const strategyCode = this.generateStrategyCode();
|
||||
const vectorResult = await this.vectorEngine.executeVectorizedStrategy(
|
||||
dataFrame.head(this.config.warmupPeriod),
|
||||
strategyCode
|
||||
);
|
||||
|
||||
// Convert to standard format
|
||||
return this.convertVectorizedResult(vectorResult, Date.now());
|
||||
}
|
||||
|
||||
private async executeEventPhase(warmupResult: BacktestResult): Promise<BacktestResult> {
|
||||
this.logger.info('Executing event-driven phase');
|
||||
|
||||
// Set up event mode with warmup context
|
||||
this.currentIndex = this.config.warmupPeriod;
|
||||
|
||||
// Create modified context for event phase
|
||||
const eventContext: BacktestContext = {
|
||||
...this.context,
|
||||
initialPortfolio: this.extractFinalPortfolio(warmupResult)
|
||||
};
|
||||
|
||||
// Execute event-driven backtest for remaining data
|
||||
const eventMode = new EventMode(eventContext, this.eventBus);
|
||||
await eventMode.initialize();
|
||||
|
||||
// Override indicator calculations to use pre-computed values
|
||||
if (this.config.optimizeIndicators) {
|
||||
this.overrideIndicatorCalculations(eventMode);
|
||||
}
|
||||
|
||||
return await eventMode.execute();
|
||||
}
|
||||
|
||||
private async precomputeIndicators(dataFrame: DataFrame): Promise<void> {
|
||||
this.logger.info('Pre-computing indicators vectorized');
|
||||
|
||||
const close = dataFrame.getColumn('close');
|
||||
const high = dataFrame.getColumn('high');
|
||||
const low = dataFrame.getColumn('low');
|
||||
|
||||
// Import technical indicators from vector engine
|
||||
const { TechnicalIndicators } = await import('@stock-bot/vector-engine');
|
||||
|
||||
// Pre-compute common indicators
|
||||
this.precomputedIndicators.set('sma_20', TechnicalIndicators.sma(close, 20));
|
||||
this.precomputedIndicators.set('sma_50', TechnicalIndicators.sma(close, 50));
|
||||
this.precomputedIndicators.set('ema_12', TechnicalIndicators.ema(close, 12));
|
||||
this.precomputedIndicators.set('ema_26', TechnicalIndicators.ema(close, 26));
|
||||
this.precomputedIndicators.set('rsi', TechnicalIndicators.rsi(close));
|
||||
this.precomputedIndicators.set('atr', TechnicalIndicators.atr(high, low, close));
|
||||
|
||||
const macd = TechnicalIndicators.macd(close);
|
||||
this.precomputedIndicators.set('macd', macd.macd);
|
||||
this.precomputedIndicators.set('macd_signal', macd.signal);
|
||||
this.precomputedIndicators.set('macd_histogram', macd.histogram);
|
||||
|
||||
const bb = TechnicalIndicators.bollingerBands(close);
|
||||
this.precomputedIndicators.set('bb_upper', bb.upper);
|
||||
this.precomputedIndicators.set('bb_middle', bb.middle);
|
||||
this.precomputedIndicators.set('bb_lower', bb.lower);
|
||||
|
||||
this.logger.info('Indicators pre-computed', {
|
||||
indicators: Array.from(this.precomputedIndicators.keys())
|
||||
});
|
||||
}
|
||||
|
||||
private overrideIndicatorCalculations(eventMode: EventMode): void {
|
||||
// Override the event mode's indicator calculations to use pre-computed values
|
||||
// This is a simplified approach - in production you'd want a more sophisticated interface
|
||||
const originalCalculateIndicators = (eventMode as any).calculateIndicators;
|
||||
|
||||
(eventMode as any).calculateIndicators = (symbol: string, index: number) => {
|
||||
const indicators: Record<string, number> = {};
|
||||
|
||||
for (const [name, values] of this.precomputedIndicators.entries()) {
|
||||
if (index < values.length) {
|
||||
indicators[name] = values[index];
|
||||
}
|
||||
}
|
||||
|
||||
return indicators;
|
||||
};
|
||||
}
|
||||
|
||||
private async estimateDataSize(): Promise<number> {
|
||||
// Estimate the number of data points for the backtest period
|
||||
const startTime = new Date(this.context.startDate).getTime();
|
||||
const endTime = new Date(this.context.endDate).getTime();
|
||||
const timeRange = endTime - startTime;
|
||||
|
||||
// Assume 1-minute intervals (60000ms)
|
||||
const estimatedPoints = Math.floor(timeRange / 60000);
|
||||
|
||||
this.logger.debug('Estimated data size', {
|
||||
timeRange,
|
||||
estimatedPoints,
|
||||
threshold: this.config.vectorizedThreshold
|
||||
});
|
||||
|
||||
return estimatedPoints;
|
||||
}
|
||||
|
||||
private async loadWarmupData(): Promise<any[]> {
|
||||
// Load historical data for warmup phase
|
||||
// This should load more data than just the warmup period for indicator calculations
|
||||
const data = [];
|
||||
const startTime = new Date(this.context.startDate).getTime();
|
||||
const warmupEndTime = startTime + (this.config.warmupPeriod * 60000);
|
||||
|
||||
// Add extra lookback for indicator calculations
|
||||
const lookbackTime = startTime - (200 * 60000); // 200 periods lookback
|
||||
|
||||
for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) {
|
||||
const basePrice = 100 + Math.sin(timestamp / 1000000) * 10;
|
||||
const volatility = 0.02;
|
||||
|
||||
const open = basePrice + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const close = open + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const high = Math.max(open, close) + Math.random() * volatility * basePrice;
|
||||
const low = Math.min(open, close) - Math.random() * volatility * basePrice;
|
||||
const volume = Math.floor(Math.random() * 10000) + 1000;
|
||||
|
||||
data.push({
|
||||
timestamp,
|
||||
symbol: this.context.symbol,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private createDataFrame(data: any[]): DataFrame {
|
||||
return new DataFrame(data, {
|
||||
columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'],
|
||||
dtypes: {
|
||||
timestamp: 'number',
|
||||
symbol: 'string',
|
||||
open: 'number',
|
||||
high: 'number',
|
||||
low: 'number',
|
||||
close: 'number',
|
||||
volume: 'number'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private generateStrategyCode(): string {
|
||||
// Generate strategy code based on context
|
||||
const strategy = this.context.strategy;
|
||||
|
||||
if (strategy.type === 'sma_crossover') {
|
||||
return 'sma_crossover';
|
||||
}
|
||||
|
||||
return strategy.code || 'sma_crossover';
|
||||
}
|
||||
|
||||
private convertVectorizedResult(vectorResult: VectorizedBacktestResult, startTime: number): BacktestResult {
|
||||
return {
|
||||
backtestId: this.context.backtestId,
|
||||
strategy: this.context.strategy,
|
||||
symbol: this.context.symbol,
|
||||
startDate: this.context.startDate,
|
||||
endDate: this.context.endDate,
|
||||
mode: 'hybrid-vectorized',
|
||||
duration: Date.now() - startTime,
|
||||
trades: vectorResult.trades.map(trade => ({
|
||||
id: `trade_${trade.entryIndex}_${trade.exitIndex}`,
|
||||
symbol: this.context.symbol,
|
||||
side: trade.side,
|
||||
entryTime: vectorResult.timestamps[trade.entryIndex],
|
||||
exitTime: vectorResult.timestamps[trade.exitIndex],
|
||||
entryPrice: trade.entryPrice,
|
||||
exitPrice: trade.exitPrice,
|
||||
quantity: trade.quantity,
|
||||
pnl: trade.pnl,
|
||||
commission: 0,
|
||||
slippage: 0
|
||||
})),
|
||||
performance: {
|
||||
totalReturn: vectorResult.metrics.totalReturns,
|
||||
sharpeRatio: vectorResult.metrics.sharpeRatio,
|
||||
maxDrawdown: vectorResult.metrics.maxDrawdown,
|
||||
winRate: vectorResult.metrics.winRate,
|
||||
profitFactor: vectorResult.metrics.profitFactor,
|
||||
totalTrades: vectorResult.metrics.totalTrades,
|
||||
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
|
||||
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
|
||||
avgTrade: vectorResult.metrics.avgTrade,
|
||||
avgWin: vectorResult.trades.filter(t => t.pnl > 0)
|
||||
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0,
|
||||
avgLoss: vectorResult.trades.filter(t => t.pnl <= 0)
|
||||
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl <= 0).length || 0,
|
||||
largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0)
|
||||
},
|
||||
equity: vectorResult.equity,
|
||||
drawdown: vectorResult.metrics.drawdown,
|
||||
metadata: {
|
||||
mode: 'hybrid-vectorized',
|
||||
dataPoints: vectorResult.timestamps.length,
|
||||
signals: Object.keys(vectorResult.signals),
|
||||
optimizations: ['vectorized_warmup', 'precomputed_indicators']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private extractFinalPortfolio(warmupResult: BacktestResult): any {
|
||||
// Extract the final portfolio state from warmup phase
|
||||
const finalEquity = warmupResult.equity[warmupResult.equity.length - 1] || 10000;
|
||||
|
||||
return {
|
||||
cash: finalEquity,
|
||||
positions: [], // Simplified - in production would track actual positions
|
||||
equity: finalEquity
|
||||
};
|
||||
}
|
||||
|
||||
private combineResults(warmupResult: BacktestResult, eventResult: BacktestResult, startTime: number): BacktestResult {
|
||||
// Combine results from both phases
|
||||
const combinedTrades = [...warmupResult.trades, ...eventResult.trades];
|
||||
const combinedEquity = [...warmupResult.equity, ...eventResult.equity];
|
||||
const combinedDrawdown = [...(warmupResult.drawdown || []), ...(eventResult.drawdown || [])];
|
||||
|
||||
// Recalculate combined performance metrics
|
||||
const totalPnL = combinedTrades.reduce((sum, trade) => sum + trade.pnl, 0);
|
||||
const winningTrades = combinedTrades.filter(t => t.pnl > 0);
|
||||
const losingTrades = combinedTrades.filter(t => t.pnl <= 0);
|
||||
|
||||
const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
|
||||
const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
|
||||
|
||||
return {
|
||||
backtestId: this.context.backtestId,
|
||||
strategy: this.context.strategy,
|
||||
symbol: this.context.symbol,
|
||||
startDate: this.context.startDate,
|
||||
endDate: this.context.endDate,
|
||||
mode: 'hybrid',
|
||||
duration: Date.now() - startTime,
|
||||
trades: combinedTrades,
|
||||
performance: {
|
||||
totalReturn: (combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0],
|
||||
sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation
|
||||
maxDrawdown: Math.max(...combinedDrawdown),
|
||||
winRate: winningTrades.length / combinedTrades.length,
|
||||
profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity,
|
||||
totalTrades: combinedTrades.length,
|
||||
winningTrades: winningTrades.length,
|
||||
losingTrades: losingTrades.length,
|
||||
avgTrade: totalPnL / combinedTrades.length,
|
||||
avgWin: grossProfit / winningTrades.length || 0,
|
||||
avgLoss: grossLoss / losingTrades.length || 0,
|
||||
largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0)
|
||||
},
|
||||
equity: combinedEquity,
|
||||
drawdown: combinedDrawdown,
|
||||
metadata: {
|
||||
mode: 'hybrid',
|
||||
phases: ['vectorized-warmup', 'event-driven'],
|
||||
warmupPeriod: this.config.warmupPeriod,
|
||||
optimizations: ['precomputed_indicators', 'hybrid_execution'],
|
||||
warmupTrades: warmupResult.trades.length,
|
||||
eventTrades: eventResult.trades.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await super.cleanup();
|
||||
await this.eventMode.cleanup();
|
||||
await this.vectorizedMode.cleanup();
|
||||
this.precomputedIndicators.clear();
|
||||
this.logger.info('Hybrid mode cleanup completed');
|
||||
}
|
||||
}
|
||||
|
||||
export default HybridMode;
|
||||
31
apps/strategy-service/src/backtesting/modes/live-mode.ts
Normal file
31
apps/strategy-service/src/backtesting/modes/live-mode.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Live Trading Mode
|
||||
* Executes orders through real brokers
|
||||
*/
|
||||
import { ExecutionMode, Order, OrderResult, MarketData } from '../../framework/execution-mode';
|
||||
|
||||
export class LiveMode extends ExecutionMode {
|
||||
name = 'live';
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
this.logger.info('Executing live order', { orderId: order.id });
|
||||
|
||||
// TODO: Implement real broker integration
|
||||
// This will connect to actual brokerage APIs
|
||||
throw new Error('Live broker integration not implemented yet');
|
||||
}
|
||||
|
||||
getCurrentTime(): Date {
|
||||
return new Date(); // Real time
|
||||
}
|
||||
|
||||
async getMarketData(symbol: string): Promise<MarketData> {
|
||||
// TODO: Get live market data
|
||||
throw new Error('Live market data fetching not implemented yet');
|
||||
}
|
||||
|
||||
async publishEvent(event: string, data: any): Promise<void> {
|
||||
// TODO: Publish to real event bus (Dragonfly)
|
||||
this.logger.debug('Publishing event', { event, data });
|
||||
}
|
||||
}
|
||||
239
apps/strategy-service/src/backtesting/modes/vectorized-mode.ts
Normal file
239
apps/strategy-service/src/backtesting/modes/vectorized-mode.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import { createLogger } from '@stock-bot/logger';
|
||||
import { EventBus } from '@stock-bot/event-bus';
|
||||
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
|
||||
import { DataFrame } from '@stock-bot/data-frame';
|
||||
import { ExecutionMode, BacktestContext, BacktestResult } from '../framework/execution-mode';
|
||||
|
||||
export interface VectorizedModeConfig {
|
||||
batchSize?: number;
|
||||
enableOptimization?: boolean;
|
||||
parallelProcessing?: boolean;
|
||||
}
|
||||
|
||||
export class VectorizedMode extends ExecutionMode {
|
||||
private vectorEngine: VectorEngine;
|
||||
private config: VectorizedModeConfig;
|
||||
private logger = createLogger('vectorized-mode');
|
||||
|
||||
constructor(
|
||||
context: BacktestContext,
|
||||
eventBus: EventBus,
|
||||
config: VectorizedModeConfig = {}
|
||||
) {
|
||||
super(context, eventBus);
|
||||
this.vectorEngine = new VectorEngine();
|
||||
this.config = {
|
||||
batchSize: 10000,
|
||||
enableOptimization: true,
|
||||
parallelProcessing: true,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
this.logger.info('Vectorized mode initialized', {
|
||||
backtestId: this.context.backtestId,
|
||||
config: this.config
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<BacktestResult> {
|
||||
const startTime = Date.now();
|
||||
this.logger.info('Starting vectorized backtest execution');
|
||||
|
||||
try {
|
||||
// Load all data at once for vectorized processing
|
||||
const data = await this.loadHistoricalData();
|
||||
|
||||
// Convert to DataFrame format
|
||||
const dataFrame = this.createDataFrame(data);
|
||||
|
||||
// Execute vectorized strategy
|
||||
const strategyCode = this.generateStrategyCode();
|
||||
const vectorResult = await this.vectorEngine.executeVectorizedStrategy(
|
||||
dataFrame,
|
||||
strategyCode
|
||||
);
|
||||
|
||||
// Convert to standard backtest result format
|
||||
const result = this.convertVectorizedResult(vectorResult, startTime);
|
||||
|
||||
// Emit completion event
|
||||
await this.eventBus.publishBacktestUpdate(
|
||||
this.context.backtestId,
|
||||
100,
|
||||
{ status: 'completed', result }
|
||||
);
|
||||
|
||||
this.logger.info('Vectorized backtest completed', {
|
||||
backtestId: this.context.backtestId,
|
||||
duration: Date.now() - startTime,
|
||||
totalTrades: result.trades.length
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Vectorized backtest failed', {
|
||||
error,
|
||||
backtestId: this.context.backtestId
|
||||
});
|
||||
|
||||
await this.eventBus.publishBacktestUpdate(
|
||||
this.context.backtestId,
|
||||
0,
|
||||
{ status: 'failed', error: error.message }
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadHistoricalData(): Promise<any[]> {
|
||||
// Load all historical data at once
|
||||
// This is much more efficient than loading tick by tick
|
||||
const data = [];
|
||||
|
||||
// Simulate loading data (in production, this would be a bulk database query)
|
||||
const startTime = new Date(this.context.startDate).getTime();
|
||||
const endTime = new Date(this.context.endDate).getTime();
|
||||
const interval = 60000; // 1 minute intervals
|
||||
|
||||
for (let timestamp = startTime; timestamp <= endTime; timestamp += interval) {
|
||||
// Simulate OHLCV data
|
||||
const basePrice = 100 + Math.sin(timestamp / 1000000) * 10;
|
||||
const volatility = 0.02;
|
||||
|
||||
const open = basePrice + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const close = open + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const high = Math.max(open, close) + Math.random() * volatility * basePrice;
|
||||
const low = Math.min(open, close) - Math.random() * volatility * basePrice;
|
||||
const volume = Math.floor(Math.random() * 10000) + 1000;
|
||||
|
||||
data.push({
|
||||
timestamp,
|
||||
symbol: this.context.symbol,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private createDataFrame(data: any[]): DataFrame {
|
||||
return new DataFrame(data, {
|
||||
columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'],
|
||||
dtypes: {
|
||||
timestamp: 'number',
|
||||
symbol: 'string',
|
||||
open: 'number',
|
||||
high: 'number',
|
||||
low: 'number',
|
||||
close: 'number',
|
||||
volume: 'number'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private generateStrategyCode(): string {
|
||||
// Convert strategy configuration to vectorized strategy code
|
||||
// This is a simplified example - in production you'd have a more sophisticated compiler
|
||||
const strategy = this.context.strategy;
|
||||
|
||||
if (strategy.type === 'sma_crossover') {
|
||||
return 'sma_crossover';
|
||||
}
|
||||
|
||||
// Add more strategy types as needed
|
||||
return strategy.code || 'sma_crossover';
|
||||
}
|
||||
|
||||
private convertVectorizedResult(
|
||||
vectorResult: VectorizedBacktestResult,
|
||||
startTime: number
|
||||
): BacktestResult {
|
||||
return {
|
||||
backtestId: this.context.backtestId,
|
||||
strategy: this.context.strategy,
|
||||
symbol: this.context.symbol,
|
||||
startDate: this.context.startDate,
|
||||
endDate: this.context.endDate,
|
||||
mode: 'vectorized',
|
||||
duration: Date.now() - startTime,
|
||||
trades: vectorResult.trades.map(trade => ({
|
||||
id: `trade_${trade.entryIndex}_${trade.exitIndex}`,
|
||||
symbol: this.context.symbol,
|
||||
side: trade.side,
|
||||
entryTime: vectorResult.timestamps[trade.entryIndex],
|
||||
exitTime: vectorResult.timestamps[trade.exitIndex],
|
||||
entryPrice: trade.entryPrice,
|
||||
exitPrice: trade.exitPrice,
|
||||
quantity: trade.quantity,
|
||||
pnl: trade.pnl,
|
||||
commission: 0, // Simplified
|
||||
slippage: 0
|
||||
})),
|
||||
performance: {
|
||||
totalReturn: vectorResult.metrics.totalReturns,
|
||||
sharpeRatio: vectorResult.metrics.sharpeRatio,
|
||||
maxDrawdown: vectorResult.metrics.maxDrawdown,
|
||||
winRate: vectorResult.metrics.winRate,
|
||||
profitFactor: vectorResult.metrics.profitFactor,
|
||||
totalTrades: vectorResult.metrics.totalTrades,
|
||||
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
|
||||
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
|
||||
avgTrade: vectorResult.metrics.avgTrade,
|
||||
avgWin: vectorResult.trades.filter(t => t.pnl > 0)
|
||||
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0,
|
||||
avgLoss: vectorResult.trades.filter(t => t.pnl <= 0)
|
||||
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl <= 0).length || 0,
|
||||
largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0)
|
||||
},
|
||||
equity: vectorResult.equity,
|
||||
drawdown: vectorResult.metrics.drawdown,
|
||||
metadata: {
|
||||
mode: 'vectorized',
|
||||
dataPoints: vectorResult.timestamps.length,
|
||||
signals: Object.keys(vectorResult.signals),
|
||||
optimizations: this.config.enableOptimization ? ['vectorized_computation'] : []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await super.cleanup();
|
||||
this.logger.info('Vectorized mode cleanup completed');
|
||||
}
|
||||
|
||||
// Batch processing capabilities
|
||||
async batchBacktest(strategies: Array<{ id: string; config: any }>): Promise<Record<string, BacktestResult>> {
|
||||
this.logger.info('Starting batch vectorized backtest', {
|
||||
strategiesCount: strategies.length
|
||||
});
|
||||
|
||||
const data = await this.loadHistoricalData();
|
||||
const dataFrame = this.createDataFrame(data);
|
||||
|
||||
const strategyConfigs = strategies.map(s => ({
|
||||
id: s.id,
|
||||
code: this.generateStrategyCode()
|
||||
}));
|
||||
|
||||
const batchResults = await this.vectorEngine.batchBacktest(dataFrame, strategyConfigs);
|
||||
const results: Record<string, BacktestResult> = {};
|
||||
|
||||
for (const [strategyId, vectorResult] of Object.entries(batchResults)) {
|
||||
results[strategyId] = this.convertVectorizedResult(vectorResult, Date.now());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export default VectorizedMode;
|
||||
300
apps/strategy-service/src/cli/index.ts
Normal file
300
apps/strategy-service/src/cli/index.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Strategy Service CLI
|
||||
* Command-line interface for running backtests and managing strategies
|
||||
*/
|
||||
|
||||
import { program } from 'commander';
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { createEventBus } from '@stock-bot/event-bus';
|
||||
import { BacktestContext } from '../framework/execution-mode';
|
||||
import LiveMode from '../backtesting/modes/live-mode';
|
||||
import EventMode from '../backtesting/modes/event-mode';
|
||||
import VectorizedMode from '../backtesting/modes/vectorized-mode';
|
||||
import HybridMode from '../backtesting/modes/hybrid-mode';
|
||||
|
||||
const logger = createLogger('strategy-cli');
|
||||
|
||||
interface CLIBacktestConfig {
|
||||
strategy: string;
|
||||
symbol: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
mode: 'live' | 'event' | 'vectorized' | 'hybrid';
|
||||
initialCapital?: number;
|
||||
config?: string;
|
||||
output?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
async function runBacktest(options: CLIBacktestConfig): Promise<void> {
|
||||
logger.info('Starting backtest from CLI', { options });
|
||||
|
||||
try {
|
||||
// Initialize event bus
|
||||
const eventBus = createEventBus({
|
||||
serviceName: 'strategy-cli',
|
||||
enablePersistence: false // Disable Redis for CLI
|
||||
});
|
||||
|
||||
// Create backtest context
|
||||
const context: BacktestContext = {
|
||||
backtestId: `cli_${Date.now()}`,
|
||||
strategy: {
|
||||
id: options.strategy,
|
||||
name: options.strategy,
|
||||
type: options.strategy,
|
||||
code: options.strategy,
|
||||
parameters: {}
|
||||
},
|
||||
symbol: options.symbol,
|
||||
startDate: options.startDate,
|
||||
endDate: options.endDate,
|
||||
initialCapital: options.initialCapital || 10000,
|
||||
mode: options.mode
|
||||
};
|
||||
|
||||
// Load additional config if provided
|
||||
if (options.config) {
|
||||
const configData = await loadConfig(options.config);
|
||||
context.strategy.parameters = { ...context.strategy.parameters, ...configData };
|
||||
}
|
||||
|
||||
// Create and execute the appropriate mode
|
||||
let executionMode;
|
||||
|
||||
switch (options.mode) {
|
||||
case 'live':
|
||||
executionMode = new LiveMode(context, eventBus);
|
||||
break;
|
||||
case 'event':
|
||||
executionMode = new EventMode(context, eventBus);
|
||||
break;
|
||||
case 'vectorized':
|
||||
executionMode = new VectorizedMode(context, eventBus);
|
||||
break;
|
||||
case 'hybrid':
|
||||
executionMode = new HybridMode(context, eventBus);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown execution mode: ${options.mode}`);
|
||||
}
|
||||
|
||||
// Subscribe to progress updates
|
||||
eventBus.subscribe('backtest.update', (message) => {
|
||||
const { backtestId, progress, ...data } = message.data;
|
||||
console.log(`Progress: ${progress}%`, data);
|
||||
});
|
||||
|
||||
await executionMode.initialize();
|
||||
const result = await executionMode.execute();
|
||||
await executionMode.cleanup();
|
||||
|
||||
// Display results
|
||||
displayResults(result);
|
||||
|
||||
// Save results if output specified
|
||||
if (options.output) {
|
||||
await saveResults(result, options.output);
|
||||
}
|
||||
|
||||
await eventBus.close();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Backtest failed', { error });
|
||||
console.error('Backtest failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig(configPath: string): Promise<any> {
|
||||
try {
|
||||
if (configPath.endsWith('.json')) {
|
||||
const file = Bun.file(configPath);
|
||||
return await file.json();
|
||||
} else {
|
||||
// Assume it's a JavaScript/TypeScript module
|
||||
return await import(configPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load config', { configPath, error });
|
||||
throw new Error(`Failed to load config from ${configPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(result: any): void {
|
||||
console.log('\n=== Backtest Results ===');
|
||||
console.log(`Strategy: ${result.strategy.name}`);
|
||||
console.log(`Symbol: ${result.symbol}`);
|
||||
console.log(`Period: ${result.startDate} to ${result.endDate}`);
|
||||
console.log(`Mode: ${result.mode}`);
|
||||
console.log(`Duration: ${result.duration}ms`);
|
||||
|
||||
console.log('\n--- Performance ---');
|
||||
console.log(`Total Return: ${(result.performance.totalReturn * 100).toFixed(2)}%`);
|
||||
console.log(`Sharpe Ratio: ${result.performance.sharpeRatio.toFixed(3)}`);
|
||||
console.log(`Max Drawdown: ${(result.performance.maxDrawdown * 100).toFixed(2)}%`);
|
||||
console.log(`Win Rate: ${(result.performance.winRate * 100).toFixed(1)}%`);
|
||||
console.log(`Profit Factor: ${result.performance.profitFactor.toFixed(2)}`);
|
||||
|
||||
console.log('\n--- Trading Stats ---');
|
||||
console.log(`Total Trades: ${result.performance.totalTrades}`);
|
||||
console.log(`Winning Trades: ${result.performance.winningTrades}`);
|
||||
console.log(`Losing Trades: ${result.performance.losingTrades}`);
|
||||
console.log(`Average Trade: ${result.performance.avgTrade.toFixed(2)}`);
|
||||
console.log(`Average Win: ${result.performance.avgWin.toFixed(2)}`);
|
||||
console.log(`Average Loss: ${result.performance.avgLoss.toFixed(2)}`);
|
||||
console.log(`Largest Win: ${result.performance.largestWin.toFixed(2)}`);
|
||||
console.log(`Largest Loss: ${result.performance.largestLoss.toFixed(2)}`);
|
||||
|
||||
if (result.metadata) {
|
||||
console.log('\n--- Metadata ---');
|
||||
Object.entries(result.metadata).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${Array.isArray(value) ? value.join(', ') : value}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveResults(result: any, outputPath: string): Promise<void> {
|
||||
try {
|
||||
if (outputPath.endsWith('.json')) {
|
||||
await Bun.write(outputPath, JSON.stringify(result, null, 2));
|
||||
} else if (outputPath.endsWith('.csv')) {
|
||||
const csv = convertTradesToCSV(result.trades);
|
||||
await Bun.write(outputPath, csv);
|
||||
} else {
|
||||
// Default to JSON
|
||||
await Bun.write(outputPath + '.json', JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
console.log(`\nResults saved to: ${outputPath}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to save results', { outputPath, error });
|
||||
console.error(`Failed to save results: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function convertTradesToCSV(trades: any[]): string {
|
||||
if (trades.length === 0) return 'No trades executed\n';
|
||||
|
||||
const headers = Object.keys(trades[0]).join(',');
|
||||
const rows = trades.map(trade =>
|
||||
Object.values(trade).map(value =>
|
||||
typeof value === 'string' ? `"${value}"` : value
|
||||
).join(',')
|
||||
);
|
||||
|
||||
return [headers, ...rows].join('\n');
|
||||
}
|
||||
|
||||
async function listStrategies(): Promise<void> {
|
||||
console.log('Available strategies:');
|
||||
console.log(' sma_crossover - Simple Moving Average Crossover');
|
||||
console.log(' ema_crossover - Exponential Moving Average Crossover');
|
||||
console.log(' rsi_mean_reversion - RSI Mean Reversion');
|
||||
console.log(' macd_trend - MACD Trend Following');
|
||||
console.log(' bollinger_bands - Bollinger Bands Strategy');
|
||||
// Add more as they're implemented
|
||||
}
|
||||
|
||||
async function validateStrategy(strategy: string): Promise<void> {
|
||||
console.log(`Validating strategy: ${strategy}`);
|
||||
|
||||
// TODO: Add strategy validation logic
|
||||
// This could check if the strategy exists, has valid parameters, etc.
|
||||
|
||||
const validStrategies = ['sma_crossover', 'ema_crossover', 'rsi_mean_reversion', 'macd_trend', 'bollinger_bands'];
|
||||
|
||||
if (!validStrategies.includes(strategy)) {
|
||||
console.warn(`Warning: Strategy '${strategy}' is not in the list of known strategies`);
|
||||
console.log('Use --list-strategies to see available strategies');
|
||||
} else {
|
||||
console.log(`✓ Strategy '${strategy}' is valid`);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Commands
|
||||
program
|
||||
.name('strategy-cli')
|
||||
.description('Stock Trading Bot Strategy CLI')
|
||||
.version('1.0.0');
|
||||
|
||||
program
|
||||
.command('backtest')
|
||||
.description('Run a backtest')
|
||||
.requiredOption('-s, --strategy <strategy>', 'Strategy to test')
|
||||
.requiredOption('--symbol <symbol>', 'Symbol to trade')
|
||||
.requiredOption('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
||||
.requiredOption('--end-date <date>', 'End date (YYYY-MM-DD)')
|
||||
.option('-m, --mode <mode>', 'Execution mode', 'vectorized')
|
||||
.option('-c, --initial-capital <amount>', 'Initial capital', '10000')
|
||||
.option('--config <path>', 'Configuration file path')
|
||||
.option('-o, --output <path>', 'Output file path')
|
||||
.option('-v, --verbose', 'Verbose output')
|
||||
.action(async (options) => {
|
||||
await runBacktest({
|
||||
strategy: options.strategy,
|
||||
symbol: options.symbol,
|
||||
startDate: options.startDate,
|
||||
endDate: options.endDate,
|
||||
mode: options.mode,
|
||||
initialCapital: parseFloat(options.initialCapital),
|
||||
config: options.config,
|
||||
output: options.output,
|
||||
verbose: options.verbose
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('list-strategies')
|
||||
.description('List available strategies')
|
||||
.action(listStrategies);
|
||||
|
||||
program
|
||||
.command('validate')
|
||||
.description('Validate a strategy')
|
||||
.requiredOption('-s, --strategy <strategy>', 'Strategy to validate')
|
||||
.action(async (options) => {
|
||||
await validateStrategy(options.strategy);
|
||||
});
|
||||
|
||||
program
|
||||
.command('compare')
|
||||
.description('Compare multiple strategies')
|
||||
.requiredOption('--strategies <strategies>', 'Comma-separated list of strategies')
|
||||
.requiredOption('--symbol <symbol>', 'Symbol to trade')
|
||||
.requiredOption('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
||||
.requiredOption('--end-date <date>', 'End date (YYYY-MM-DD)')
|
||||
.option('-m, --mode <mode>', 'Execution mode', 'vectorized')
|
||||
.option('-c, --initial-capital <amount>', 'Initial capital', '10000')
|
||||
.option('-o, --output <path>', 'Output directory')
|
||||
.action(async (options) => {
|
||||
const strategies = options.strategies.split(',').map((s: string) => s.trim());
|
||||
console.log(`Comparing strategies: ${strategies.join(', ')}`);
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
for (const strategy of strategies) {
|
||||
console.log(`\nRunning ${strategy}...`);
|
||||
try {
|
||||
await runBacktest({
|
||||
strategy,
|
||||
symbol: options.symbol,
|
||||
startDate: options.startDate,
|
||||
endDate: options.endDate,
|
||||
mode: options.mode,
|
||||
initialCapital: parseFloat(options.initialCapital),
|
||||
output: options.output ? `${options.output}/${strategy}.json` : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to run ${strategy}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nComparison completed!');
|
||||
});
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse();
|
||||
|
||||
export { runBacktest, listStrategies, validateStrategy };
|
||||
80
apps/strategy-service/src/framework/execution-mode.ts
Normal file
80
apps/strategy-service/src/framework/execution-mode.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Execution Mode Framework
|
||||
* Base classes for different execution modes (live, event-driven, vectorized)
|
||||
*/
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = createLogger('execution-mode');
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: 'BUY' | 'SELL';
|
||||
quantity: number;
|
||||
type: 'MARKET' | 'LIMIT';
|
||||
price?: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface OrderResult {
|
||||
orderId: string;
|
||||
symbol: string;
|
||||
executedQuantity: number;
|
||||
executedPrice: number;
|
||||
commission: number;
|
||||
slippage: number;
|
||||
timestamp: Date;
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
export interface MarketData {
|
||||
symbol: string;
|
||||
timestamp: Date;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export abstract class ExecutionMode {
|
||||
protected logger = createLogger(this.constructor.name);
|
||||
|
||||
abstract name: string;
|
||||
abstract executeOrder(order: Order): Promise<OrderResult>;
|
||||
abstract getCurrentTime(): Date;
|
||||
abstract getMarketData(symbol: string): Promise<MarketData>;
|
||||
abstract publishEvent(event: string, data: any): Promise<void>;
|
||||
}
|
||||
|
||||
export enum BacktestMode {
|
||||
LIVE = 'live',
|
||||
EVENT_DRIVEN = 'event-driven',
|
||||
VECTORIZED = 'vectorized',
|
||||
HYBRID = 'hybrid'
|
||||
}
|
||||
|
||||
export class ModeFactory {
|
||||
static create(mode: BacktestMode, config?: any): ExecutionMode {
|
||||
switch (mode) {
|
||||
case BacktestMode.LIVE:
|
||||
// TODO: Import and create LiveMode
|
||||
throw new Error('LiveMode not implemented yet');
|
||||
|
||||
case BacktestMode.EVENT_DRIVEN:
|
||||
// TODO: Import and create EventBacktestMode
|
||||
throw new Error('EventBacktestMode not implemented yet');
|
||||
|
||||
case BacktestMode.VECTORIZED:
|
||||
// TODO: Import and create VectorBacktestMode
|
||||
throw new Error('VectorBacktestMode not implemented yet');
|
||||
|
||||
case BacktestMode.HYBRID:
|
||||
// TODO: Import and create HybridBacktestMode
|
||||
throw new Error('HybridBacktestMode not implemented yet');
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown mode: ${mode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
apps/strategy-service/src/index.ts
Normal file
89
apps/strategy-service/src/index.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Strategy Service - Multi-mode strategy execution and backtesting
|
||||
*/
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { Hono } from 'hono';
|
||||
import { serve } from '@hono/node-server';
|
||||
|
||||
// Load environment variables
|
||||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
const logger = createLogger('strategy-service');
|
||||
const PORT = parseInt(process.env.STRATEGY_SERVICE_PORT || '3004');
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.json({
|
||||
service: 'strategy-service',
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Strategy execution endpoints
|
||||
app.post('/api/strategy/run', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Strategy run request', {
|
||||
strategy: body.strategy,
|
||||
mode: body.mode
|
||||
});
|
||||
|
||||
// TODO: Implement strategy execution
|
||||
return c.json({
|
||||
message: 'Strategy execution endpoint - not implemented yet',
|
||||
strategy: body.strategy,
|
||||
mode: body.mode
|
||||
});
|
||||
});
|
||||
|
||||
// Backtesting endpoints
|
||||
app.post('/api/backtest/event', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Event-driven backtest request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement event-driven backtesting
|
||||
return c.json({
|
||||
message: 'Event-driven backtest endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/backtest/vector', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Vectorized backtest request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement vectorized backtesting
|
||||
return c.json({
|
||||
message: 'Vectorized backtest endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/backtest/hybrid', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Hybrid backtest request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement hybrid backtesting
|
||||
return c.json({
|
||||
message: 'Hybrid backtest endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
// Parameter optimization endpoint
|
||||
app.post('/api/optimize', async (c) => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Parameter optimization request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement parameter optimization
|
||||
return c.json({
|
||||
message: 'Parameter optimization endpoint - not implemented yet'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: PORT,
|
||||
});
|
||||
|
||||
logger.info(`Strategy Service started on port ${PORT}`);
|
||||
23
libs/data-frame/package.json
Normal file
23
libs/data-frame/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@stock-bot/data-frame",
|
||||
"version": "1.0.0",
|
||||
"description": "DataFrame library for time series data manipulation",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun-types": "*"
|
||||
}
|
||||
}
|
||||
485
libs/data-frame/src/index.ts
Normal file
485
libs/data-frame/src/index.ts
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
import { createLogger } from '@stock-bot/logger';
|
||||
|
||||
export interface DataFrameRow {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface DataFrameOptions {
|
||||
index?: string;
|
||||
columns?: string[];
|
||||
dtypes?: Record<string, 'number' | 'string' | 'boolean' | 'date'>;
|
||||
}
|
||||
|
||||
export interface GroupByResult {
|
||||
[key: string]: DataFrame;
|
||||
}
|
||||
|
||||
export interface AggregationFunction {
|
||||
(values: any[]): any;
|
||||
}
|
||||
|
||||
export class DataFrame {
|
||||
private data: DataFrameRow[];
|
||||
private _columns: string[];
|
||||
private _index: string;
|
||||
private _dtypes: Record<string, 'number' | 'string' | 'boolean' | 'date'>;
|
||||
private logger = createLogger('dataframe');
|
||||
|
||||
constructor(data: DataFrameRow[] = [], options: DataFrameOptions = {}) {
|
||||
this.data = [...data];
|
||||
this._index = options.index || 'index';
|
||||
this._columns = options.columns || this.inferColumns();
|
||||
this._dtypes = options.dtypes || {};
|
||||
|
||||
this.validateAndCleanData();
|
||||
}
|
||||
|
||||
private inferColumns(): string[] {
|
||||
if (this.data.length === 0) return [];
|
||||
|
||||
const columns = new Set<string>();
|
||||
for (const row of this.data) {
|
||||
Object.keys(row).forEach(key => columns.add(key));
|
||||
}
|
||||
|
||||
return Array.from(columns).sort();
|
||||
}
|
||||
|
||||
private validateAndCleanData(): void {
|
||||
if (this.data.length === 0) return;
|
||||
|
||||
// Ensure all rows have the same columns
|
||||
for (let i = 0; i < this.data.length; i++) {
|
||||
const row = this.data[i];
|
||||
|
||||
// Add missing columns with null values
|
||||
for (const col of this._columns) {
|
||||
if (!(col in row)) {
|
||||
row[col] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply data type conversions
|
||||
for (const [col, dtype] of Object.entries(this._dtypes)) {
|
||||
if (col in row && row[col] !== null) {
|
||||
row[col] = this.convertValue(row[col], dtype);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private convertValue(value: any, dtype: string): any {
|
||||
switch (dtype) {
|
||||
case 'number':
|
||||
return typeof value === 'number' ? value : parseFloat(value);
|
||||
case 'string':
|
||||
return String(value);
|
||||
case 'boolean':
|
||||
return Boolean(value);
|
||||
case 'date':
|
||||
return value instanceof Date ? value : new Date(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Basic properties
|
||||
get columns(): string[] {
|
||||
return [...this._columns];
|
||||
}
|
||||
|
||||
get index(): string {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.data.length;
|
||||
}
|
||||
|
||||
get shape(): [number, number] {
|
||||
return [this.data.length, this._columns.length];
|
||||
}
|
||||
|
||||
get empty(): boolean {
|
||||
return this.data.length === 0;
|
||||
}
|
||||
|
||||
// Data access methods
|
||||
head(n: number = 5): DataFrame {
|
||||
return new DataFrame(this.data.slice(0, n), {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
tail(n: number = 5): DataFrame {
|
||||
return new DataFrame(this.data.slice(-n), {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
iloc(start: number, end?: number): DataFrame {
|
||||
const slice = end !== undefined ? this.data.slice(start, end) : this.data.slice(start);
|
||||
return new DataFrame(slice, {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
at(index: number, column: string): any {
|
||||
if (index < 0 || index >= this.data.length) {
|
||||
throw new Error(`Index ${index} out of bounds`);
|
||||
}
|
||||
return this.data[index][column];
|
||||
}
|
||||
|
||||
// Column operations
|
||||
select(columns: string[]): DataFrame {
|
||||
const validColumns = columns.filter(col => this._columns.includes(col));
|
||||
const newData = this.data.map(row => {
|
||||
const newRow: DataFrameRow = {};
|
||||
for (const col of validColumns) {
|
||||
newRow[col] = row[col];
|
||||
}
|
||||
return newRow;
|
||||
});
|
||||
|
||||
return new DataFrame(newData, {
|
||||
columns: validColumns,
|
||||
index: this._index,
|
||||
dtypes: this.filterDtypes(validColumns)
|
||||
});
|
||||
}
|
||||
|
||||
drop(columns: string[]): DataFrame {
|
||||
const remainingColumns = this._columns.filter(col => !columns.includes(col));
|
||||
return this.select(remainingColumns);
|
||||
}
|
||||
|
||||
getColumn(column: string): any[] {
|
||||
if (!this._columns.includes(column)) {
|
||||
throw new Error(`Column '${column}' not found`);
|
||||
}
|
||||
return this.data.map(row => row[column]);
|
||||
}
|
||||
|
||||
setColumn(column: string, values: any[]): DataFrame {
|
||||
if (values.length !== this.data.length) {
|
||||
throw new Error('Values length must match DataFrame length');
|
||||
}
|
||||
|
||||
const newData = this.data.map((row, index) => ({
|
||||
...row,
|
||||
[column]: values[index]
|
||||
}));
|
||||
|
||||
const newColumns = this._columns.includes(column)
|
||||
? this._columns
|
||||
: [...this._columns, column];
|
||||
|
||||
return new DataFrame(newData, {
|
||||
columns: newColumns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
// Filtering
|
||||
filter(predicate: (row: DataFrameRow, index: number) => boolean): DataFrame {
|
||||
const filteredData = this.data.filter(predicate);
|
||||
return new DataFrame(filteredData, {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
where(column: string, operator: '>' | '<' | '>=' | '<=' | '==' | '!=', value: any): DataFrame {
|
||||
return this.filter(row => {
|
||||
const cellValue = row[column];
|
||||
switch (operator) {
|
||||
case '>': return cellValue > value;
|
||||
case '<': return cellValue < value;
|
||||
case '>=': return cellValue >= value;
|
||||
case '<=': return cellValue <= value;
|
||||
case '==': return cellValue === value;
|
||||
case '!=': return cellValue !== value;
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sorting
|
||||
sort(column: string, ascending: boolean = true): DataFrame {
|
||||
const sortedData = [...this.data].sort((a, b) => {
|
||||
const aVal = a[column];
|
||||
const bVal = b[column];
|
||||
|
||||
if (aVal === bVal) return 0;
|
||||
|
||||
const comparison = aVal > bVal ? 1 : -1;
|
||||
return ascending ? comparison : -comparison;
|
||||
});
|
||||
|
||||
return new DataFrame(sortedData, {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregation
|
||||
groupBy(column: string): GroupByResult {
|
||||
const groups: Record<string, DataFrameRow[]> = {};
|
||||
|
||||
for (const row of this.data) {
|
||||
const key = String(row[column]);
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(row);
|
||||
}
|
||||
|
||||
const result: GroupByResult = {};
|
||||
for (const [key, rows] of Object.entries(groups)) {
|
||||
result[key] = new DataFrame(rows, {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
agg(aggregations: Record<string, AggregationFunction>): DataFrameRow {
|
||||
const result: DataFrameRow = {};
|
||||
|
||||
for (const [column, func] of Object.entries(aggregations)) {
|
||||
if (!this._columns.includes(column)) {
|
||||
throw new Error(`Column '${column}' not found`);
|
||||
}
|
||||
|
||||
const values = this.getColumn(column).filter(val => val !== null && val !== undefined);
|
||||
result[column] = func(values);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Statistical methods
|
||||
mean(column: string): number {
|
||||
const values = this.getColumn(column).filter(val => typeof val === 'number');
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
|
||||
sum(column: string): number {
|
||||
const values = this.getColumn(column).filter(val => typeof val === 'number');
|
||||
return values.reduce((sum, val) => sum + val, 0);
|
||||
}
|
||||
|
||||
min(column: string): number {
|
||||
const values = this.getColumn(column).filter(val => typeof val === 'number');
|
||||
return Math.min(...values);
|
||||
}
|
||||
|
||||
max(column: string): number {
|
||||
const values = this.getColumn(column).filter(val => typeof val === 'number');
|
||||
return Math.max(...values);
|
||||
}
|
||||
|
||||
std(column: string): number {
|
||||
const values = this.getColumn(column).filter(val => typeof val === 'number');
|
||||
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
// Time series specific methods
|
||||
resample(timeColumn: string, frequency: string): DataFrame {
|
||||
// Simple resampling implementation
|
||||
// For production, you'd want more sophisticated time-based grouping
|
||||
const sorted = this.sort(timeColumn);
|
||||
|
||||
switch (frequency) {
|
||||
case '1H':
|
||||
return this.resampleByHour(sorted, timeColumn);
|
||||
case '1D':
|
||||
return this.resampleByDay(sorted, timeColumn);
|
||||
default:
|
||||
throw new Error(`Unsupported frequency: ${frequency}`);
|
||||
}
|
||||
}
|
||||
|
||||
private resampleByHour(sorted: DataFrame, timeColumn: string): DataFrame {
|
||||
const groups: Record<string, DataFrameRow[]> = {};
|
||||
|
||||
for (const row of sorted.data) {
|
||||
const date = new Date(row[timeColumn]);
|
||||
const hourKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getHours()}`;
|
||||
|
||||
if (!groups[hourKey]) {
|
||||
groups[hourKey] = [];
|
||||
}
|
||||
groups[hourKey].push(row);
|
||||
}
|
||||
|
||||
const aggregatedData: DataFrameRow[] = [];
|
||||
for (const [key, rows] of Object.entries(groups)) {
|
||||
const tempDf = new DataFrame(rows, {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
|
||||
// Create OHLCV aggregation
|
||||
const aggregated: DataFrameRow = {
|
||||
[timeColumn]: rows[0][timeColumn],
|
||||
open: rows[0].close || rows[0].price,
|
||||
high: tempDf.max('high') || tempDf.max('close') || tempDf.max('price'),
|
||||
low: tempDf.min('low') || tempDf.min('close') || tempDf.min('price'),
|
||||
close: rows[rows.length - 1].close || rows[rows.length - 1].price,
|
||||
volume: tempDf.sum('volume') || 0
|
||||
};
|
||||
|
||||
aggregatedData.push(aggregated);
|
||||
}
|
||||
|
||||
return new DataFrame(aggregatedData);
|
||||
}
|
||||
|
||||
private resampleByDay(sorted: DataFrame, timeColumn: string): DataFrame {
|
||||
// Similar to resampleByHour but group by day
|
||||
const groups: Record<string, DataFrameRow[]> = {};
|
||||
|
||||
for (const row of sorted.data) {
|
||||
const date = new Date(row[timeColumn]);
|
||||
const dayKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
|
||||
if (!groups[dayKey]) {
|
||||
groups[dayKey] = [];
|
||||
}
|
||||
groups[dayKey].push(row);
|
||||
}
|
||||
|
||||
const aggregatedData: DataFrameRow[] = [];
|
||||
for (const [key, rows] of Object.entries(groups)) {
|
||||
const tempDf = new DataFrame(rows, {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: this._dtypes
|
||||
});
|
||||
|
||||
const aggregated: DataFrameRow = {
|
||||
[timeColumn]: rows[0][timeColumn],
|
||||
open: rows[0].close || rows[0].price,
|
||||
high: tempDf.max('high') || tempDf.max('close') || tempDf.max('price'),
|
||||
low: tempDf.min('low') || tempDf.min('close') || tempDf.min('price'),
|
||||
close: rows[rows.length - 1].close || rows[rows.length - 1].price,
|
||||
volume: tempDf.sum('volume') || 0
|
||||
};
|
||||
|
||||
aggregatedData.push(aggregated);
|
||||
}
|
||||
|
||||
return new DataFrame(aggregatedData);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
copy(): DataFrame {
|
||||
return new DataFrame(this.data.map(row => ({ ...row })), {
|
||||
columns: this._columns,
|
||||
index: this._index,
|
||||
dtypes: { ...this._dtypes }
|
||||
});
|
||||
}
|
||||
|
||||
concat(other: DataFrame): DataFrame {
|
||||
const combinedData = [...this.data, ...other.data];
|
||||
const combinedColumns = Array.from(new Set([...this._columns, ...other._columns]));
|
||||
|
||||
return new DataFrame(combinedData, {
|
||||
columns: combinedColumns,
|
||||
index: this._index,
|
||||
dtypes: { ...this._dtypes, ...other._dtypes }
|
||||
});
|
||||
}
|
||||
|
||||
toArray(): DataFrameRow[] {
|
||||
return this.data.map(row => ({ ...row }));
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return JSON.stringify(this.data);
|
||||
}
|
||||
|
||||
private filterDtypes(columns: string[]): Record<string, 'number' | 'string' | 'boolean' | 'date'> {
|
||||
const filtered: Record<string, 'number' | 'string' | 'boolean' | 'date'> = {};
|
||||
for (const col of columns) {
|
||||
if (this._dtypes[col]) {
|
||||
filtered[col] = this._dtypes[col];
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Display method
|
||||
toString(): string {
|
||||
if (this.empty) {
|
||||
return 'Empty DataFrame';
|
||||
}
|
||||
|
||||
const maxRows = 10;
|
||||
const displayData = this.data.slice(0, maxRows);
|
||||
|
||||
let result = `DataFrame (${this.length} rows x ${this._columns.length} columns)\n`;
|
||||
result += this._columns.join('\t') + '\n';
|
||||
result += '-'.repeat(this._columns.join('\t').length) + '\n';
|
||||
|
||||
for (const row of displayData) {
|
||||
const values = this._columns.map(col => String(row[col] ?? 'null'));
|
||||
result += values.join('\t') + '\n';
|
||||
}
|
||||
|
||||
if (this.length > maxRows) {
|
||||
result += `... (${this.length - maxRows} more rows)\n`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
export function createDataFrame(data: DataFrameRow[], options?: DataFrameOptions): DataFrame {
|
||||
return new DataFrame(data, options);
|
||||
}
|
||||
|
||||
export function readCSV(csvData: string, options?: DataFrameOptions): DataFrame {
|
||||
const lines = csvData.trim().split('\n');
|
||||
if (lines.length === 0) {
|
||||
return new DataFrame();
|
||||
}
|
||||
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
const data: DataFrameRow[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(',').map(v => v.trim());
|
||||
const row: DataFrameRow = {};
|
||||
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
row[headers[j]] = values[j] || null;
|
||||
}
|
||||
|
||||
data.push(row);
|
||||
}
|
||||
|
||||
return new DataFrame(data, {
|
||||
columns: headers,
|
||||
...options
|
||||
});
|
||||
}
|
||||
16
libs/data-frame/tsconfig.json
Normal file
16
libs/data-frame/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../logger" },
|
||||
{ "path": "../types" }
|
||||
]
|
||||
}
|
||||
25
libs/event-bus/package.json
Normal file
25
libs/event-bus/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@stock-bot/event-bus",
|
||||
"version": "1.0.0",
|
||||
"description": "Event bus library for inter-service communication",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"ioredis": "^5.3.2",
|
||||
"eventemitter3": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun-types": "*"
|
||||
}
|
||||
}
|
||||
169
libs/event-bus/src/index.ts
Normal file
169
libs/event-bus/src/index.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { EventEmitter } from 'eventemitter3';
|
||||
import Redis from 'ioredis';
|
||||
import { createLogger } from '@stock-bot/logger';
|
||||
import { dragonflyConfig } from '@stock-bot/config';
|
||||
|
||||
export interface EventBusMessage {
|
||||
id: string;
|
||||
type: string;
|
||||
source: string;
|
||||
timestamp: number;
|
||||
data: any;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EventHandler<T = any> {
|
||||
(message: EventBusMessage & { data: T }): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface EventBusOptions {
|
||||
|
||||
serviceName: string;
|
||||
enablePersistence?: boolean;
|
||||
}
|
||||
|
||||
export class EventBus extends EventEmitter {
|
||||
private redis?: Redis;
|
||||
private subscriber?: Redis;
|
||||
private serviceName: string;
|
||||
private logger
|
||||
private enablePersistence: boolean;
|
||||
|
||||
constructor(options: EventBusOptions) {
|
||||
super();
|
||||
this.serviceName = options.serviceName;
|
||||
this.enablePersistence = options.enablePersistence ?? true;
|
||||
this.logger = createLogger(`event-bus:${this.serviceName}`);
|
||||
|
||||
this.redis = new Redis({
|
||||
host: dragonflyConfig.DRAGONFLY_HOST,
|
||||
port: dragonflyConfig.DRAGONFLY_PORT,
|
||||
password: dragonflyConfig.DRAGONFLY_PASSWORD,
|
||||
db: dragonflyConfig.DRAGONFLY_DATABASE || 0,
|
||||
maxRetriesPerRequest: dragonflyConfig.DRAGONFLY_MAX_RETRIES,
|
||||
});
|
||||
|
||||
this.subscriber = new Redis({
|
||||
host: dragonflyConfig.DRAGONFLY_HOST,
|
||||
port: dragonflyConfig.DRAGONFLY_PORT,
|
||||
password: dragonflyConfig.DRAGONFLY_PASSWORD,
|
||||
db: dragonflyConfig.DRAGONFLY_DATABASE || 0,
|
||||
});
|
||||
|
||||
this.subscriber.on('message', this.handleRedisMessage.bind(this));
|
||||
this.logger.info('Redis event bus initialized');
|
||||
}
|
||||
|
||||
private handleRedisMessage(channel: string, message: string) {
|
||||
try {
|
||||
const eventMessage: EventBusMessage = JSON.parse(message);
|
||||
|
||||
// Don't process our own messages
|
||||
if (eventMessage.source === this.serviceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit(eventMessage.type, eventMessage);
|
||||
this.logger.debug(`Received event: ${eventMessage.type} from ${eventMessage.source}`);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to parse Redis message', { error, message });
|
||||
}
|
||||
}
|
||||
|
||||
async publish<T = any>(type: string, data: T, metadata?: Record<string, any>): Promise<void> {
|
||||
const message: EventBusMessage = {
|
||||
id: this.generateId(),
|
||||
type,
|
||||
source: this.serviceName,
|
||||
timestamp: Date.now(),
|
||||
data,
|
||||
metadata,
|
||||
};
|
||||
|
||||
// Emit locally first
|
||||
this.emit(type, message);
|
||||
|
||||
// Publish to Redis if available
|
||||
if (this.redis && this.enablePersistence) {
|
||||
try {
|
||||
await this.redis.publish(`events:${type}`, JSON.stringify(message));
|
||||
this.logger.debug(`Published event: ${type}`, { messageId: message.id });
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to publish event: ${type}`, { error, messageId: message.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe<T = any>(eventType: string, handler: EventHandler<T>): Promise<void> {
|
||||
// Subscribe locally
|
||||
this.on(eventType, handler);
|
||||
|
||||
// Subscribe to Redis channel if available
|
||||
if (this.subscriber && this.enablePersistence) {
|
||||
try {
|
||||
await this.subscriber.subscribe(`events:${eventType}`);
|
||||
this.logger.debug(`Subscribed to event: ${eventType}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to subscribe to event: ${eventType}`, { error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe(eventType: string, handler?: EventHandler): Promise<void> {
|
||||
if (handler) {
|
||||
this.off(eventType, handler);
|
||||
} else {
|
||||
this.removeAllListeners(eventType);
|
||||
}
|
||||
|
||||
if (this.subscriber && this.enablePersistence) {
|
||||
try {
|
||||
await this.subscriber.unsubscribe(`events:${eventType}`);
|
||||
this.logger.debug(`Unsubscribed from event: ${eventType}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to unsubscribe from event: ${eventType}`, { error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.redis) {
|
||||
await this.redis.quit();
|
||||
}
|
||||
if (this.subscriber) {
|
||||
await this.subscriber.quit();
|
||||
}
|
||||
this.removeAllListeners();
|
||||
this.logger.info('Event bus closed');
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${this.serviceName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// Utility methods for common event patterns
|
||||
async publishMarketData(symbol: string, data: any): Promise<void> {
|
||||
await this.publish('market.data', { symbol, ...data });
|
||||
}
|
||||
|
||||
async publishOrderUpdate(orderId: string, status: string, data: any): Promise<void> {
|
||||
await this.publish('order.update', { orderId, status, ...data });
|
||||
}
|
||||
|
||||
async publishStrategySignal(strategyId: string, signal: any): Promise<void> {
|
||||
await this.publish('strategy.signal', { strategyId, ...signal });
|
||||
}
|
||||
|
||||
async publishPortfolioUpdate(portfolioId: string, data: any): Promise<void> {
|
||||
await this.publish('portfolio.update', { portfolioId, ...data });
|
||||
}
|
||||
|
||||
async publishBacktestUpdate(backtestId: string, progress: number, data?: any): Promise<void> {
|
||||
await this.publish('backtest.update', { backtestId, progress, ...data });
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function for easy initialization
|
||||
export function createEventBus(options: EventBusOptions): EventBus {
|
||||
return new EventBus(options);
|
||||
}
|
||||
16
libs/event-bus/tsconfig.json
Normal file
16
libs/event-bus/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../logger" },
|
||||
{ "path": "../types" }
|
||||
]
|
||||
}
|
||||
27
libs/strategy-engine/package.json
Normal file
27
libs/strategy-engine/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@stock-bot/strategy-engine",
|
||||
"version": "1.0.0",
|
||||
"description": "Strategy execution engine with multi-mode support",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/config": "workspace:*",
|
||||
"@stock-bot/utils": "workspace:*",
|
||||
"@stock-bot/event-bus": "workspace:*",
|
||||
"@stock-bot/data-frame": "workspace:*",
|
||||
"eventemitter3": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun-types": "*"
|
||||
}
|
||||
}
|
||||
370
libs/strategy-engine/src/index.ts
Normal file
370
libs/strategy-engine/src/index.ts
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { EventEmitter } from 'eventemitter3';
|
||||
import { createLogger, Logger } from '@stock-bot/logger';
|
||||
import { EventBus } from '@stock-bot/event-bus';
|
||||
import { DataFrame } from '@stock-bot/data-frame';
|
||||
|
||||
// Core types
|
||||
export interface MarketData {
|
||||
symbol: string;
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface TradingSignal {
|
||||
type: 'BUY' | 'SELL' | 'HOLD';
|
||||
symbol: string;
|
||||
timestamp: number;
|
||||
price: number;
|
||||
quantity: number;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface StrategyContext {
|
||||
symbol: string;
|
||||
timeframe: string;
|
||||
data: DataFrame;
|
||||
indicators: Record<string, any>;
|
||||
position?: Position;
|
||||
portfolio: PortfolioSummary;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
unrealizedPnL: number;
|
||||
side: 'LONG' | 'SHORT';
|
||||
}
|
||||
|
||||
export interface PortfolioSummary {
|
||||
totalValue: number;
|
||||
cash: number;
|
||||
positions: Position[];
|
||||
totalPnL: number;
|
||||
dayPnL: number;
|
||||
}
|
||||
|
||||
export interface StrategyConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
symbols: string[];
|
||||
timeframes: string[];
|
||||
parameters: Record<string, any>;
|
||||
riskLimits: RiskLimits;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RiskLimits {
|
||||
maxPositionSize: number;
|
||||
maxDailyLoss: number;
|
||||
maxDrawdown: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
}
|
||||
|
||||
// Abstract base strategy class
|
||||
export abstract class BaseStrategy extends EventEmitter {
|
||||
protected logger;
|
||||
protected eventBus: EventBus;
|
||||
protected config: StrategyConfig;
|
||||
protected isActive: boolean = false;
|
||||
|
||||
constructor(config: StrategyConfig, eventBus: EventBus) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.eventBus = eventBus;
|
||||
this.logger = createLogger(`strategy:${config.id}`);
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by concrete strategies
|
||||
abstract initialize(): Promise<void>;
|
||||
abstract onMarketData(context: StrategyContext): Promise<TradingSignal[]>;
|
||||
abstract onSignal(signal: TradingSignal): Promise<void>;
|
||||
abstract cleanup(): Promise<void>;
|
||||
|
||||
// Optional lifecycle methods
|
||||
onStart?(): Promise<void>;
|
||||
onStop?(): Promise<void>;
|
||||
onError?(error: Error): Promise<void>;
|
||||
|
||||
// Control methods
|
||||
async start(): Promise<void> {
|
||||
if (this.isActive) {
|
||||
this.logger.warn('Strategy already active');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.initialize();
|
||||
|
||||
if (this.onStart) {
|
||||
await this.onStart();
|
||||
}
|
||||
|
||||
this.isActive = true;
|
||||
this.logger.info('Strategy started', { strategyId: this.config.id });
|
||||
this.emit('started');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to start strategy', { error, strategyId: this.config.id });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isActive) {
|
||||
this.logger.warn('Strategy not active');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.onStop) {
|
||||
await this.onStop();
|
||||
}
|
||||
|
||||
await this.cleanup();
|
||||
this.isActive = false;
|
||||
this.logger.info('Strategy stopped', { strategyId: this.config.id });
|
||||
this.emit('stopped');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to stop strategy', { error, strategyId: this.config.id });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
protected async emitSignal(signal: TradingSignal): Promise<void> {
|
||||
await this.eventBus.publishStrategySignal(this.config.id, signal);
|
||||
this.emit('signal', signal);
|
||||
this.logger.info('Signal generated', {
|
||||
signal: signal.type,
|
||||
symbol: signal.symbol,
|
||||
confidence: signal.confidence
|
||||
});
|
||||
}
|
||||
|
||||
protected checkRiskLimits(signal: TradingSignal, context: StrategyContext): boolean {
|
||||
const limits = this.config.riskLimits;
|
||||
|
||||
// Check position size limit
|
||||
if (signal.quantity > limits.maxPositionSize) {
|
||||
this.logger.warn('Signal exceeds max position size', {
|
||||
requested: signal.quantity,
|
||||
limit: limits.maxPositionSize
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check daily loss limit
|
||||
if (context.portfolio.dayPnL <= -limits.maxDailyLoss) {
|
||||
this.logger.warn('Daily loss limit reached', {
|
||||
dayPnL: context.portfolio.dayPnL,
|
||||
limit: -limits.maxDailyLoss
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Getters
|
||||
get id(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
get active(): boolean {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
get configuration(): StrategyConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy execution engine
|
||||
export class StrategyEngine extends EventEmitter {
|
||||
private strategies: Map<string, BaseStrategy> = new Map();
|
||||
private logger;
|
||||
private eventBus: EventBus;
|
||||
private isRunning: boolean = false;
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
super();
|
||||
this.eventBus = eventBus;
|
||||
this.logger = createLogger('strategy-engine');
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Subscribe to market data events
|
||||
await this.eventBus.subscribe('market.data', this.handleMarketData.bind(this));
|
||||
await this.eventBus.subscribe('order.update', this.handleOrderUpdate.bind(this));
|
||||
await this.eventBus.subscribe('portfolio.update', this.handlePortfolioUpdate.bind(this));
|
||||
|
||||
this.logger.info('Strategy engine initialized');
|
||||
}
|
||||
|
||||
async registerStrategy(strategy: BaseStrategy): Promise<void> {
|
||||
if (this.strategies.has(strategy.id)) {
|
||||
throw new Error(`Strategy ${strategy.id} already registered`);
|
||||
}
|
||||
|
||||
this.strategies.set(strategy.id, strategy);
|
||||
|
||||
// Forward strategy events
|
||||
strategy.on('signal', (signal) => this.emit('signal', signal));
|
||||
strategy.on('error', (error) => this.emit('error', error));
|
||||
|
||||
this.logger.info('Strategy registered', { strategyId: strategy.id });
|
||||
}
|
||||
|
||||
async unregisterStrategy(strategyId: string): Promise<void> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
if (strategy.active) {
|
||||
await strategy.stop();
|
||||
}
|
||||
|
||||
strategy.removeAllListeners();
|
||||
this.strategies.delete(strategyId);
|
||||
|
||||
this.logger.info('Strategy unregistered', { strategyId });
|
||||
}
|
||||
|
||||
async startStrategy(strategyId: string): Promise<void> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
await strategy.start();
|
||||
}
|
||||
|
||||
async stopStrategy(strategyId: string): Promise<void> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
await strategy.stop();
|
||||
}
|
||||
|
||||
async startAll(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('Engine already running');
|
||||
return;
|
||||
}
|
||||
|
||||
const startPromises = Array.from(this.strategies.values())
|
||||
.filter(strategy => strategy.configuration.enabled)
|
||||
.map(strategy => strategy.start());
|
||||
|
||||
await Promise.all(startPromises);
|
||||
this.isRunning = true;
|
||||
this.logger.info('All strategies started');
|
||||
this.emit('started');
|
||||
}
|
||||
|
||||
async stopAll(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
this.logger.warn('Engine not running');
|
||||
return;
|
||||
}
|
||||
|
||||
const stopPromises = Array.from(this.strategies.values())
|
||||
.filter(strategy => strategy.active)
|
||||
.map(strategy => strategy.stop());
|
||||
|
||||
await Promise.all(stopPromises);
|
||||
this.isRunning = false;
|
||||
this.logger.info('All strategies stopped');
|
||||
this.emit('stopped');
|
||||
}
|
||||
|
||||
private async handleMarketData(message: any): Promise<void> {
|
||||
const { symbol, ...data } = message.data;
|
||||
|
||||
// Find strategies that trade this symbol
|
||||
const relevantStrategies = Array.from(this.strategies.values())
|
||||
.filter(strategy =>
|
||||
strategy.active &&
|
||||
strategy.configuration.symbols.includes(symbol)
|
||||
);
|
||||
|
||||
for (const strategy of relevantStrategies) {
|
||||
try {
|
||||
// Create context for this strategy
|
||||
const context: StrategyContext = {
|
||||
symbol,
|
||||
timeframe: '1m', // TODO: Get from strategy config
|
||||
data: new DataFrame([data]), // TODO: Use historical data
|
||||
indicators: {},
|
||||
portfolio: {
|
||||
totalValue: 100000, // TODO: Get real portfolio data
|
||||
cash: 50000,
|
||||
positions: [],
|
||||
totalPnL: 0,
|
||||
dayPnL: 0
|
||||
},
|
||||
timestamp: data.timestamp
|
||||
};
|
||||
|
||||
const signals = await strategy.onMarketData(context);
|
||||
|
||||
for (const signal of signals) {
|
||||
await strategy.onSignal(signal);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error processing market data for strategy', {
|
||||
error,
|
||||
strategyId: strategy.id,
|
||||
symbol
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOrderUpdate(message: any): Promise<void> {
|
||||
// Handle order updates - notify relevant strategies
|
||||
this.logger.debug('Order update received', { data: message.data });
|
||||
}
|
||||
|
||||
private async handlePortfolioUpdate(message: any): Promise<void> {
|
||||
// Handle portfolio updates - notify relevant strategies
|
||||
this.logger.debug('Portfolio update received', { data: message.data });
|
||||
}
|
||||
|
||||
getStrategy(strategyId: string): BaseStrategy | undefined {
|
||||
return this.strategies.get(strategyId);
|
||||
}
|
||||
|
||||
getStrategies(): BaseStrategy[] {
|
||||
return Array.from(this.strategies.values());
|
||||
}
|
||||
|
||||
getActiveStrategies(): BaseStrategy[] {
|
||||
return this.getStrategies().filter(strategy => strategy.active);
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
await this.stopAll();
|
||||
this.strategies.clear();
|
||||
this.removeAllListeners();
|
||||
this.logger.info('Strategy engine shutdown');
|
||||
}
|
||||
}
|
||||
19
libs/strategy-engine/tsconfig.json
Normal file
19
libs/strategy-engine/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../logger" },
|
||||
{ "path": "../types" },
|
||||
{ "path": "../event-bus" },
|
||||
{ "path": "../data-frame" },
|
||||
{ "path": "../http-client" },
|
||||
]
|
||||
}
|
||||
24
libs/vector-engine/package.json
Normal file
24
libs/vector-engine/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@stock-bot/vector-engine",
|
||||
"version": "1.0.0",
|
||||
"description": "Vectorized computation engine for high-performance backtesting",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/logger": "workspace:*",
|
||||
"@stock-bot/utils": "workspace:*",
|
||||
"@stock-bot/data-frame": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun-types": "*"
|
||||
}
|
||||
}
|
||||
393
libs/vector-engine/src/index.ts
Normal file
393
libs/vector-engine/src/index.ts
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { createLogger } from '@stock-bot/logger';
|
||||
import { DataFrame } from '@stock-bot/data-frame';
|
||||
import { atr, sma, ema, rsi, macd, bollingerBands } from '@stock-bot/utils';
|
||||
|
||||
// Vector operations interface
|
||||
export interface VectorOperation {
|
||||
name: string;
|
||||
inputs: string[];
|
||||
output: string;
|
||||
operation: (inputs: number[][]) => number[];
|
||||
}
|
||||
|
||||
// Vectorized strategy context
|
||||
export interface VectorizedContext {
|
||||
data: DataFrame;
|
||||
lookback: number;
|
||||
indicators: Record<string, number[]>;
|
||||
signals: Record<string, number[]>;
|
||||
}
|
||||
|
||||
// Performance metrics for vectorized backtesting
|
||||
export interface VectorizedMetrics {
|
||||
totalReturns: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
winRate: number;
|
||||
profitFactor: number;
|
||||
totalTrades: number;
|
||||
avgTrade: number;
|
||||
returns: number[];
|
||||
drawdown: number[];
|
||||
equity: number[];
|
||||
}
|
||||
|
||||
// Vectorized backtest result
|
||||
export interface VectorizedBacktestResult {
|
||||
metrics: VectorizedMetrics;
|
||||
trades: VectorizedTrade[];
|
||||
equity: number[];
|
||||
timestamps: number[];
|
||||
signals: Record<string, number[]>;
|
||||
}
|
||||
|
||||
export interface VectorizedTrade {
|
||||
entryIndex: number;
|
||||
exitIndex: number;
|
||||
entryPrice: number;
|
||||
exitPrice: number;
|
||||
quantity: number;
|
||||
side: 'LONG' | 'SHORT';
|
||||
pnl: number;
|
||||
return: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
// Vectorized strategy engine
|
||||
export class VectorEngine {
|
||||
private logger = createLogger('vector-engine');
|
||||
private operations: Map<string, VectorOperation> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.registerDefaultOperations();
|
||||
}
|
||||
|
||||
private registerDefaultOperations(): void {
|
||||
// Register common mathematical operations
|
||||
this.registerOperation({
|
||||
name: 'add',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => a.map((val, i) => val + b[i])
|
||||
});
|
||||
|
||||
this.registerOperation({
|
||||
name: 'subtract',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => a.map((val, i) => val - b[i])
|
||||
});
|
||||
|
||||
this.registerOperation({
|
||||
name: 'multiply',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => a.map((val, i) => val * b[i])
|
||||
});
|
||||
|
||||
this.registerOperation({
|
||||
name: 'divide',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => a.map((val, i) => b[i] !== 0 ? val / b[i] : NaN)
|
||||
});
|
||||
|
||||
// Register comparison operations
|
||||
this.registerOperation({
|
||||
name: 'greater_than',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => a.map((val, i) => val > b[i] ? 1 : 0)
|
||||
});
|
||||
|
||||
this.registerOperation({
|
||||
name: 'less_than',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => a.map((val, i) => val < b[i] ? 1 : 0)
|
||||
});
|
||||
|
||||
this.registerOperation({
|
||||
name: 'crossover',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => {
|
||||
const result = new Array(a.length).fill(0);
|
||||
for (let i = 1; i < a.length; i++) {
|
||||
if (a[i] > b[i] && a[i - 1] <= b[i - 1]) {
|
||||
result[i] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
this.registerOperation({
|
||||
name: 'crossunder',
|
||||
inputs: ['a', 'b'],
|
||||
output: 'result',
|
||||
operation: ([a, b]) => {
|
||||
const result = new Array(a.length).fill(0);
|
||||
for (let i = 1; i < a.length; i++) {
|
||||
if (a[i] < b[i] && a[i - 1] >= b[i - 1]) {
|
||||
result[i] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerOperation(operation: VectorOperation): void {
|
||||
this.operations.set(operation.name, operation);
|
||||
this.logger.debug(`Registered operation: ${operation.name}`);
|
||||
}
|
||||
|
||||
// Execute vectorized strategy
|
||||
async executeVectorizedStrategy(
|
||||
data: DataFrame,
|
||||
strategyCode: string
|
||||
): Promise<VectorizedBacktestResult> {
|
||||
try {
|
||||
const context = this.prepareContext(data);
|
||||
const signals = this.executeStrategy(context, strategyCode);
|
||||
const trades = this.generateTrades(data, signals);
|
||||
const metrics = this.calculateMetrics(data, trades);
|
||||
|
||||
return {
|
||||
metrics,
|
||||
trades,
|
||||
equity: metrics.equity,
|
||||
timestamps: data.getColumn('timestamp'),
|
||||
signals
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Vectorized strategy execution failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private prepareContext(data: DataFrame): VectorizedContext {
|
||||
const close = data.getColumn('close');
|
||||
const high = data.getColumn('high');
|
||||
const low = data.getColumn('low');
|
||||
const volume = data.getColumn('volume');
|
||||
|
||||
// Calculate common indicators
|
||||
const indicators: Record<string, number[]> = {
|
||||
sma_20: sma(close, 20),
|
||||
sma_50: sma(close, 50),
|
||||
ema_12: ema(close, 12),
|
||||
ema_26: ema(close, 26),
|
||||
rsi: rsi(close),
|
||||
};
|
||||
|
||||
const m = macd(close);
|
||||
indicators.macd = m.macd;
|
||||
indicators.macd_signal = m.signal;
|
||||
indicators.macd_histogram = m.histogram;
|
||||
|
||||
const bb = bollingerBands(close);
|
||||
indicators.bb_upper = bb.upper;
|
||||
indicators.bb_middle = bb.middle;
|
||||
indicators.bb_lower = bb.lower;
|
||||
|
||||
return {
|
||||
data,
|
||||
lookback: 100,
|
||||
indicators,
|
||||
signals: {}
|
||||
};
|
||||
}
|
||||
|
||||
private executeStrategy(context: VectorizedContext, strategyCode: string): Record<string, number[]> {
|
||||
// This is a simplified strategy execution
|
||||
// In production, you'd want a more sophisticated strategy compiler/interpreter
|
||||
const signals: Record<string, number[]> = {
|
||||
buy: new Array(context.data.length).fill(0),
|
||||
sell: new Array(context.data.length).fill(0)
|
||||
};
|
||||
|
||||
// Example: Simple moving average crossover strategy
|
||||
if (strategyCode.includes('sma_crossover')) {
|
||||
const sma20 = context.indicators.sma_20;
|
||||
const sma50 = context.indicators.sma_50;
|
||||
|
||||
for (let i = 1; i < sma20.length; i++) {
|
||||
// Buy signal: SMA20 crosses above SMA50
|
||||
if (!isNaN(sma20[i]) && !isNaN(sma50[i]) &&
|
||||
!isNaN(sma20[i-1]) && !isNaN(sma50[i-1])) {
|
||||
if (sma20[i] > sma50[i] && sma20[i-1] <= sma50[i-1]) {
|
||||
signals.buy[i] = 1;
|
||||
}
|
||||
// Sell signal: SMA20 crosses below SMA50
|
||||
else if (sma20[i] < sma50[i] && sma20[i-1] >= sma50[i-1]) {
|
||||
signals.sell[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
private generateTrades(data: DataFrame, signals: Record<string, number[]>): VectorizedTrade[] {
|
||||
const trades: VectorizedTrade[] = [];
|
||||
const close = data.getColumn('close');
|
||||
const timestamps = data.getColumn('timestamp');
|
||||
|
||||
let position: { index: number; price: number; side: 'LONG' | 'SHORT' } | null = null;
|
||||
|
||||
for (let i = 0; i < close.length; i++) {
|
||||
if (signals.buy[i] === 1 && !position) {
|
||||
// Open long position
|
||||
position = {
|
||||
index: i,
|
||||
price: close[i],
|
||||
side: 'LONG'
|
||||
};
|
||||
} else if (signals.sell[i] === 1) {
|
||||
if (position && position.side === 'LONG') {
|
||||
// Close long position
|
||||
const trade: VectorizedTrade = {
|
||||
entryIndex: position.index,
|
||||
exitIndex: i,
|
||||
entryPrice: position.price,
|
||||
exitPrice: close[i],
|
||||
quantity: 1, // Simplified: always trade 1 unit
|
||||
side: 'LONG',
|
||||
pnl: close[i] - position.price,
|
||||
return: (close[i] - position.price) / position.price,
|
||||
duration: timestamps[i] - timestamps[position.index]
|
||||
};
|
||||
trades.push(trade);
|
||||
position = null;
|
||||
} else if (!position) {
|
||||
// Open short position
|
||||
position = {
|
||||
index: i,
|
||||
price: close[i],
|
||||
side: 'SHORT'
|
||||
};
|
||||
}
|
||||
} else if (signals.buy[i] === 1 && position && position.side === 'SHORT') {
|
||||
// Close short position
|
||||
const trade: VectorizedTrade = {
|
||||
entryIndex: position.index,
|
||||
exitIndex: i,
|
||||
entryPrice: position.price,
|
||||
exitPrice: close[i],
|
||||
quantity: 1,
|
||||
side: 'SHORT',
|
||||
pnl: position.price - close[i],
|
||||
return: (position.price - close[i]) / position.price,
|
||||
duration: timestamps[i] - timestamps[position.index]
|
||||
};
|
||||
trades.push(trade);
|
||||
position = null;
|
||||
}
|
||||
}
|
||||
|
||||
return trades;
|
||||
}
|
||||
|
||||
private calculateMetrics(data: DataFrame, trades: VectorizedTrade[]): VectorizedMetrics {
|
||||
if (trades.length === 0) {
|
||||
return {
|
||||
totalReturns: 0,
|
||||
sharpeRatio: 0,
|
||||
maxDrawdown: 0,
|
||||
winRate: 0,
|
||||
profitFactor: 0,
|
||||
totalTrades: 0,
|
||||
avgTrade: 0,
|
||||
returns: [],
|
||||
drawdown: [],
|
||||
equity: []
|
||||
};
|
||||
}
|
||||
|
||||
const returns = trades.map(t => t.return);
|
||||
const pnls = trades.map(t => t.pnl);
|
||||
|
||||
// Calculate equity curve
|
||||
const equity: number[] = [10000]; // Starting capital
|
||||
let currentEquity = 10000;
|
||||
|
||||
for (const trade of trades) {
|
||||
currentEquity += trade.pnl;
|
||||
equity.push(currentEquity);
|
||||
}
|
||||
|
||||
// Calculate drawdown
|
||||
const drawdown: number[] = [];
|
||||
let peak = equity[0];
|
||||
|
||||
for (const eq of equity) {
|
||||
if (eq > peak) peak = eq;
|
||||
drawdown.push((peak - eq) / peak);
|
||||
}
|
||||
|
||||
const totalReturns = (equity[equity.length - 1] - equity[0]) / equity[0];
|
||||
const avgReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length;
|
||||
const returnStd = Math.sqrt(
|
||||
returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length
|
||||
);
|
||||
|
||||
const winningTrades = trades.filter(t => t.pnl > 0);
|
||||
const losingTrades = trades.filter(t => t.pnl < 0);
|
||||
|
||||
const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
|
||||
const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
|
||||
|
||||
return {
|
||||
totalReturns,
|
||||
sharpeRatio: returnStd !== 0 ? (avgReturn / returnStd) * Math.sqrt(252) : 0,
|
||||
maxDrawdown: Math.max(...drawdown),
|
||||
winRate: winningTrades.length / trades.length,
|
||||
profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity,
|
||||
totalTrades: trades.length,
|
||||
avgTrade: pnls.reduce((sum, pnl) => sum + pnl, 0) / trades.length,
|
||||
returns,
|
||||
drawdown,
|
||||
equity
|
||||
};
|
||||
}
|
||||
|
||||
// Utility methods for vectorized operations
|
||||
applyOperation(operationName: string, inputs: Record<string, number[]>): number[] {
|
||||
const operation = this.operations.get(operationName);
|
||||
if (!operation) {
|
||||
throw new Error(`Operation '${operationName}' not found`);
|
||||
}
|
||||
|
||||
const inputArrays = operation.inputs.map(inputName => {
|
||||
if (!inputs[inputName]) {
|
||||
throw new Error(`Input '${inputName}' not provided for operation '${operationName}'`);
|
||||
}
|
||||
return inputs[inputName];
|
||||
});
|
||||
|
||||
return operation.operation(inputArrays);
|
||||
}
|
||||
|
||||
// Batch processing for multiple strategies
|
||||
async batchBacktest(
|
||||
data: DataFrame,
|
||||
strategies: Array<{ id: string; code: string }>
|
||||
): Promise<Record<string, VectorizedBacktestResult>> {
|
||||
const results: Record<string, VectorizedBacktestResult> = {};
|
||||
|
||||
for (const strategy of strategies) {
|
||||
try {
|
||||
this.logger.info(`Running vectorized backtest for strategy: ${strategy.id}`);
|
||||
results[strategy.id] = await this.executeVectorizedStrategy(data, strategy.code);
|
||||
} catch (error) {
|
||||
this.logger.error(`Backtest failed for strategy: ${strategy.id}`, { error });
|
||||
// Continue with other strategies
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
19
libs/vector-engine/tsconfig.json
Normal file
19
libs/vector-engine/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../logger" },
|
||||
{ "path": "../types" },
|
||||
{ "path": "../event-bus" },
|
||||
{ "path": "../data-frame" },
|
||||
{ "path": "../utils" },
|
||||
]
|
||||
}
|
||||
12
package.json
12
package.json
|
|
@ -56,6 +56,16 @@
|
|||
"bun": ">=1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensorflow/tfjs-node": "^4.22.0",
|
||||
"@tensorflow/tfjs-node-gpu": "^4.22.0",
|
||||
"valibot": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@tensorflow/tfjs-node",
|
||||
"@tensorflow/tfjs-node-gpu",
|
||||
"core-js",
|
||||
"cpu-features",
|
||||
"protobufjs",
|
||||
"ssh2"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,11 @@
|
|||
{ "path": "./libs/postgres-client" },
|
||||
{ "path": "./libs/questdb-client" },
|
||||
{ "path": "./libs/types" },
|
||||
{ "path": "./libs/utils" }
|
||||
{ "path": "./libs/utils" },
|
||||
{ "path": "./libs/event-bus" },
|
||||
{ "path": "./libs/data-frame" },
|
||||
{ "path": "./libs/strategy-engine" },
|
||||
{ "path": "./libs/vector-engine" },
|
||||
]
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue