This commit is contained in:
Bojan Kucera 2025-06-04 22:46:01 -04:00
parent 7993148a95
commit 528be93804
38 changed files with 4617 additions and 1081 deletions

View 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"
}
}

View 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}`);

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

View 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"
}

View 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
};
}
}

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

View 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' };
}
});
}
}

View 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}`);
});

View 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"
}

View 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
}
}

View 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}`);
});

View 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;
}
}

View 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"
}
}

View 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}`);

View 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
};
}
}

View 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"
}
}

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

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

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

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

View 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 };

View 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}`);
}
}
}

View 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}`);

1278
bun.lock

File diff suppressed because it is too large Load diff

View 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": "*"
}
}

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

View 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" }
]
}

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

View 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" }
]
}

View 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": "*"
}
}

View 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');
}
}

View 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" },
]
}

View 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": "*"
}
}

View 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;
}
}

View 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" },
]
}

View file

@ -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"
]
}

View file

@ -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" },
]
}