restructured libs to be more aligned with core components
This commit is contained in:
parent
947b1d748d
commit
0d1be9e3cb
50 changed files with 73 additions and 67 deletions
191
libs/core/event-bus/README.md
Normal file
191
libs/core/event-bus/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# @stock-bot/event-bus
|
||||
|
||||
Lightweight event bus for inter-service communication in the Stock Bot platform.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides a simple pub/sub event system using Redis, designed for real-time event distribution between microservices. It focuses on simplicity and reliability for event-driven communication.
|
||||
|
||||
## Features
|
||||
|
||||
- Simple pub/sub pattern using Redis
|
||||
- Automatic reconnection and resubscription
|
||||
- Local event emission (works even without Redis)
|
||||
- TypeScript support with predefined trading event types
|
||||
- Lightweight with minimal dependencies
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @stock-bot/event-bus
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```typescript
|
||||
import { createEventBus, TradingEventType } from '@stock-bot/event-bus';
|
||||
|
||||
const eventBus = createEventBus({
|
||||
serviceName: 'data-ingestion',
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
enableLogging: true,
|
||||
});
|
||||
|
||||
// Wait for connection
|
||||
await eventBus.waitForConnection();
|
||||
```
|
||||
|
||||
### Publishing Events
|
||||
|
||||
```typescript
|
||||
// Publish a price update
|
||||
await eventBus.publish(TradingEventType.PRICE_UPDATE, {
|
||||
symbol: 'AAPL',
|
||||
price: 150.25,
|
||||
volume: 1000000,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// Publish with metadata
|
||||
await eventBus.publish(TradingEventType.ORDER_FILLED,
|
||||
{
|
||||
orderId: '12345',
|
||||
symbol: 'TSLA',
|
||||
side: 'buy',
|
||||
quantity: 100,
|
||||
price: 250.50,
|
||||
},
|
||||
{ source: 'ib-gateway', region: 'us' }
|
||||
);
|
||||
```
|
||||
|
||||
### Subscribing to Events
|
||||
|
||||
```typescript
|
||||
// Subscribe to price updates
|
||||
await eventBus.subscribe(TradingEventType.PRICE_UPDATE, async (message) => {
|
||||
console.log(`Price update for ${message.data.symbol}: $${message.data.price}`);
|
||||
});
|
||||
|
||||
// Subscribe to order events
|
||||
await eventBus.subscribe(TradingEventType.ORDER_FILLED, async (message) => {
|
||||
const { orderId, symbol, quantity, price } = message.data;
|
||||
console.log(`Order ${orderId} filled: ${quantity} ${symbol} @ $${price}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Event Types
|
||||
|
||||
The library includes predefined event types for common trading operations:
|
||||
|
||||
```typescript
|
||||
enum TradingEventType {
|
||||
// Market data events
|
||||
PRICE_UPDATE = 'market.price.update',
|
||||
ORDERBOOK_UPDATE = 'market.orderbook.update',
|
||||
TRADE_EXECUTED = 'market.trade.executed',
|
||||
|
||||
// Order events
|
||||
ORDER_CREATED = 'order.created',
|
||||
ORDER_FILLED = 'order.filled',
|
||||
ORDER_CANCELLED = 'order.cancelled',
|
||||
ORDER_REJECTED = 'order.rejected',
|
||||
|
||||
// Position events
|
||||
POSITION_OPENED = 'position.opened',
|
||||
POSITION_CLOSED = 'position.closed',
|
||||
POSITION_UPDATED = 'position.updated',
|
||||
|
||||
// Strategy events
|
||||
STRATEGY_SIGNAL = 'strategy.signal',
|
||||
STRATEGY_STARTED = 'strategy.started',
|
||||
STRATEGY_STOPPED = 'strategy.stopped',
|
||||
|
||||
// Risk events
|
||||
RISK_LIMIT_BREACH = 'risk.limit.breach',
|
||||
RISK_WARNING = 'risk.warning',
|
||||
|
||||
// System events
|
||||
SERVICE_STARTED = 'system.service.started',
|
||||
SERVICE_STOPPED = 'system.service.stopped',
|
||||
SERVICE_ERROR = 'system.service.error',
|
||||
}
|
||||
```
|
||||
|
||||
### Typed Events
|
||||
|
||||
Use TypeScript generics for type-safe event handling:
|
||||
|
||||
```typescript
|
||||
import type { PriceUpdateEvent, OrderEvent } from '@stock-bot/event-bus';
|
||||
|
||||
// Type-safe subscription
|
||||
await eventBus.subscribe<PriceUpdateEvent>(
|
||||
TradingEventType.PRICE_UPDATE,
|
||||
async (message) => {
|
||||
// message.data is typed as PriceUpdateEvent
|
||||
const { symbol, price, volume } = message.data;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
```typescript
|
||||
// Unsubscribe from specific event
|
||||
await eventBus.unsubscribe(TradingEventType.PRICE_UPDATE);
|
||||
|
||||
// Close all connections
|
||||
await eventBus.close();
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
This library is designed for lightweight, real-time event distribution. For reliable job processing, retries, and persistence, use the `@stock-bot/queue` library with BullMQ instead.
|
||||
|
||||
### When to Use Event Bus
|
||||
|
||||
- Real-time notifications (price updates, trade executions)
|
||||
- Service coordination (strategy signals, risk alerts)
|
||||
- System monitoring (service status, errors)
|
||||
|
||||
### When to Use Queue
|
||||
|
||||
- Data processing jobs
|
||||
- Batch operations
|
||||
- Tasks requiring persistence and retries
|
||||
- Scheduled operations
|
||||
|
||||
## Error Handling
|
||||
|
||||
The event bus handles connection failures gracefully:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await eventBus.publish(TradingEventType.PRICE_UPDATE, data);
|
||||
} catch (error) {
|
||||
// Event will still be emitted locally
|
||||
console.error('Failed to publish to Redis:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Build
|
||||
bun run build
|
||||
|
||||
# Run tests
|
||||
bun test
|
||||
|
||||
# Clean build artifacts
|
||||
bun run clean
|
||||
```
|
||||
39
libs/core/event-bus/package.json
Normal file
39
libs/core/event-bus/package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "bun test",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/logger": "*",
|
||||
"ioredis": "^5.3.2",
|
||||
"eventemitter3": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.0",
|
||||
"bun-types": "^1.2.15"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"require": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
239
libs/core/event-bus/src/event-bus.ts
Normal file
239
libs/core/event-bus/src/event-bus.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import { EventEmitter } from 'eventemitter3';
|
||||
import Redis from 'ioredis';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { EventBusConfig, EventBusMessage, EventHandler, EventSubscription } from './types';
|
||||
|
||||
/**
|
||||
* Lightweight Event Bus for inter-service communication
|
||||
* Uses Redis pub/sub for simple, real-time event distribution
|
||||
*/
|
||||
export class EventBus extends EventEmitter {
|
||||
private publisher: Redis;
|
||||
private subscriber: Redis;
|
||||
private readonly serviceName: string;
|
||||
private readonly logger: ReturnType<typeof getLogger>;
|
||||
private subscriptions: Map<string, EventSubscription> = new Map();
|
||||
private isConnected: boolean = false;
|
||||
|
||||
constructor(config: EventBusConfig) {
|
||||
super();
|
||||
this.serviceName = config.serviceName;
|
||||
this.logger = getLogger(`event-bus:${this.serviceName}`);
|
||||
|
||||
// Create Redis connections
|
||||
const redisOptions = {
|
||||
host: config.redisConfig.host,
|
||||
port: config.redisConfig.port,
|
||||
password: config.redisConfig.password,
|
||||
db: config.redisConfig.db || 0,
|
||||
lazyConnect: false,
|
||||
enableOfflineQueue: true,
|
||||
reconnectOnError: (err: Error) => {
|
||||
this.logger.error('Redis connection error:', err);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
this.publisher = new Redis(redisOptions);
|
||||
this.subscriber = new Redis(redisOptions);
|
||||
|
||||
this.setupRedisHandlers();
|
||||
}
|
||||
|
||||
private setupRedisHandlers(): void {
|
||||
// Publisher handlers
|
||||
this.publisher.on('connect', () => {
|
||||
this.logger.info('Publisher connected to Redis');
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.publisher.on('error', error => {
|
||||
this.logger.error('Publisher Redis error:', error);
|
||||
});
|
||||
|
||||
// Subscriber handlers
|
||||
this.subscriber.on('connect', () => {
|
||||
this.logger.info('Subscriber connected to Redis');
|
||||
// Resubscribe to all channels on reconnect
|
||||
this.resubscribeAll();
|
||||
});
|
||||
|
||||
this.subscriber.on('error', error => {
|
||||
this.logger.error('Subscriber Redis error:', error);
|
||||
});
|
||||
|
||||
// Handle incoming messages
|
||||
this.subscriber.on('message', this.handleMessage.bind(this));
|
||||
}
|
||||
|
||||
private handleMessage(channel: string, message: string): void {
|
||||
try {
|
||||
const eventMessage: EventBusMessage = JSON.parse(message);
|
||||
|
||||
// Skip messages from self
|
||||
if (eventMessage.source === this.serviceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract event type from channel (remove 'events:' prefix)
|
||||
const eventType = channel.replace('events:', '');
|
||||
|
||||
// Emit locally
|
||||
this.emit(eventType, eventMessage);
|
||||
|
||||
// Call registered handler if exists
|
||||
const subscription = this.subscriptions.get(eventType);
|
||||
if (subscription?.handler) {
|
||||
Promise.resolve(subscription.handler(eventMessage)).catch(error => {
|
||||
this.logger.error(`Handler error for event ${eventType}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.debug(`Received event: ${eventType} from ${eventMessage.source}`);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to handle message:', { error, channel, message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an event
|
||||
*/
|
||||
async publish<T = any>(type: string, data: T, metadata?: Record<string, any>): Promise<void> {
|
||||
const message: EventBusMessage<T> = {
|
||||
id: this.generateId(),
|
||||
type,
|
||||
source: this.serviceName,
|
||||
timestamp: Date.now(),
|
||||
data,
|
||||
metadata,
|
||||
};
|
||||
|
||||
// Emit locally first
|
||||
this.emit(type, message);
|
||||
|
||||
// Publish to Redis
|
||||
if (this.isConnected) {
|
||||
try {
|
||||
const channel = `events:${type}`;
|
||||
await this.publisher.publish(channel, 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 });
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`Not connected to Redis, event ${type} only emitted locally`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
async subscribe<T = any>(eventType: string, handler: EventHandler<T>): Promise<void> {
|
||||
// Register handler
|
||||
this.subscriptions.set(eventType, { channel: `events:${eventType}`, handler });
|
||||
|
||||
// Add local listener
|
||||
this.on(eventType, handler);
|
||||
|
||||
// Subscribe to Redis channel
|
||||
try {
|
||||
const channel = `events:${eventType}`;
|
||||
await this.subscriber.subscribe(channel);
|
||||
this.logger.debug(`Subscribed to event: ${eventType}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to subscribe to event: ${eventType}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
async unsubscribe(eventType: string, handler?: EventHandler): Promise<void> {
|
||||
// Remove specific handler or all handlers
|
||||
if (handler) {
|
||||
this.off(eventType, handler);
|
||||
} else {
|
||||
this.removeAllListeners(eventType);
|
||||
}
|
||||
|
||||
// Remove from subscriptions
|
||||
this.subscriptions.delete(eventType);
|
||||
|
||||
// Unsubscribe from Redis
|
||||
try {
|
||||
const channel = `events:${eventType}`;
|
||||
await this.subscriber.unsubscribe(channel);
|
||||
this.logger.debug(`Unsubscribed from event: ${eventType}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to unsubscribe from event: ${eventType}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resubscribe to all channels (used on reconnect)
|
||||
*/
|
||||
private async resubscribeAll(): Promise<void> {
|
||||
for (const [eventType, subscription] of this.subscriptions.entries()) {
|
||||
try {
|
||||
await this.subscriber.subscribe(subscription.channel);
|
||||
this.logger.debug(`Resubscribed to event: ${eventType}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to resubscribe to event: ${eventType}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for connection to be established
|
||||
*/
|
||||
async waitForConnection(timeout: number = 5000): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (!this.isConnected && Date.now() - startTime < timeout) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
if (!this.isConnected) {
|
||||
throw new Error(`Failed to connect to Redis within ${timeout}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all connections
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
this.isConnected = false;
|
||||
|
||||
// Clear all subscriptions
|
||||
this.subscriptions.clear();
|
||||
this.removeAllListeners();
|
||||
|
||||
// Close Redis connections
|
||||
await Promise.all([this.publisher.quit(), this.subscriber.quit()]);
|
||||
|
||||
this.logger.info('Event bus closed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique message ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return `${this.serviceName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected to Redis
|
||||
*/
|
||||
get connected(): boolean {
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service name
|
||||
*/
|
||||
get service(): string {
|
||||
return this.serviceName;
|
||||
}
|
||||
}
|
||||
13
libs/core/event-bus/src/index.ts
Normal file
13
libs/core/event-bus/src/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { EventBus } from './event-bus';
|
||||
import type { EventBusConfig } from './types';
|
||||
|
||||
/**
|
||||
* Create a new event bus instance
|
||||
*/
|
||||
export function createEventBus(config: EventBusConfig): EventBus {
|
||||
return new EventBus(config);
|
||||
}
|
||||
|
||||
// Re-export everything
|
||||
export { EventBus } from './event-bus';
|
||||
export * from './types';
|
||||
111
libs/core/event-bus/src/types.ts
Normal file
111
libs/core/event-bus/src/types.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
export interface EventBusMessage<T = any> {
|
||||
id: string;
|
||||
type: string;
|
||||
source: string;
|
||||
timestamp: number;
|
||||
data: T;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EventHandler<T = any> {
|
||||
(message: EventBusMessage<T>): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface EventBusConfig {
|
||||
serviceName: string;
|
||||
redisConfig: {
|
||||
host: string;
|
||||
port: number;
|
||||
password?: string;
|
||||
db?: number;
|
||||
};
|
||||
enableLogging?: boolean;
|
||||
}
|
||||
|
||||
export interface EventSubscription {
|
||||
channel: string;
|
||||
handler: EventHandler;
|
||||
}
|
||||
|
||||
// Trading-specific event types
|
||||
export enum TradingEventType {
|
||||
// Market data events
|
||||
PRICE_UPDATE = 'market.price.update',
|
||||
ORDERBOOK_UPDATE = 'market.orderbook.update',
|
||||
TRADE_EXECUTED = 'market.trade.executed',
|
||||
|
||||
// Order events
|
||||
ORDER_CREATED = 'order.created',
|
||||
ORDER_FILLED = 'order.filled',
|
||||
ORDER_CANCELLED = 'order.cancelled',
|
||||
ORDER_REJECTED = 'order.rejected',
|
||||
|
||||
// Position events
|
||||
POSITION_OPENED = 'position.opened',
|
||||
POSITION_CLOSED = 'position.closed',
|
||||
POSITION_UPDATED = 'position.updated',
|
||||
|
||||
// Strategy events
|
||||
STRATEGY_SIGNAL = 'strategy.signal',
|
||||
STRATEGY_STARTED = 'strategy.started',
|
||||
STRATEGY_STOPPED = 'strategy.stopped',
|
||||
|
||||
// Risk events
|
||||
RISK_LIMIT_BREACH = 'risk.limit.breach',
|
||||
RISK_WARNING = 'risk.warning',
|
||||
|
||||
// System events
|
||||
SERVICE_STARTED = 'system.service.started',
|
||||
SERVICE_STOPPED = 'system.service.stopped',
|
||||
SERVICE_ERROR = 'system.service.error',
|
||||
}
|
||||
|
||||
// Event data types
|
||||
export interface PriceUpdateEvent {
|
||||
symbol: string;
|
||||
price: number;
|
||||
volume: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface OrderEvent {
|
||||
orderId: string;
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
quantity: number;
|
||||
price?: number;
|
||||
type: 'market' | 'limit' | 'stop' | 'stop_limit';
|
||||
status: string;
|
||||
portfolioId: string;
|
||||
strategyId?: string;
|
||||
}
|
||||
|
||||
export interface PositionEvent {
|
||||
positionId: string;
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averageCost: number;
|
||||
currentPrice: number;
|
||||
unrealizedPnl: number;
|
||||
realizedPnl: number;
|
||||
portfolioId: string;
|
||||
}
|
||||
|
||||
export interface StrategySignalEvent {
|
||||
strategyId: string;
|
||||
signal: 'buy' | 'sell' | 'hold';
|
||||
symbol: string;
|
||||
confidence: number;
|
||||
indicators: Record<string, number>;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface RiskEvent {
|
||||
type: 'position_size' | 'daily_loss' | 'max_drawdown' | 'concentration';
|
||||
severity: 'warning' | 'critical';
|
||||
currentValue: number;
|
||||
limit: number;
|
||||
portfolioId?: string;
|
||||
strategyId?: string;
|
||||
message: string;
|
||||
}
|
||||
10
libs/core/event-bus/tsconfig.json
Normal file
10
libs/core/event-bus/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [{ "path": "../../core/logger" }]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue