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