fixing batch before moving to the apps
This commit is contained in:
parent
8c2f98e010
commit
3534a2c47b
14 changed files with 43 additions and 884 deletions
|
|
@ -14,7 +14,7 @@ export async function processItems<T>(
|
||||||
options: ProcessOptions
|
options: ProcessOptions
|
||||||
): Promise<BatchResult> {
|
): Promise<BatchResult> {
|
||||||
const queueManager = QueueManager.getInstance();
|
const queueManager = QueueManager.getInstance();
|
||||||
const queue = queueManager.getQueue(queueName);
|
queueManager.getQueue(queueName);
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
|
@ -61,7 +61,7 @@ async function processDirect<T>(
|
||||||
options: ProcessOptions
|
options: ProcessOptions
|
||||||
): Promise<Omit<BatchResult, 'duration'>> {
|
): Promise<Omit<BatchResult, 'duration'>> {
|
||||||
const queueManager = QueueManager.getInstance();
|
const queueManager = QueueManager.getInstance();
|
||||||
const queue = queueManager.getQueue(queueName);
|
queueManager.getQueue(queueName);
|
||||||
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
||||||
const delayPerItem = totalDelayMs / items.length;
|
const delayPerItem = totalDelayMs / items.length;
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ async function processBatched<T>(
|
||||||
options: ProcessOptions
|
options: ProcessOptions
|
||||||
): Promise<Omit<BatchResult, 'duration'>> {
|
): Promise<Omit<BatchResult, 'duration'>> {
|
||||||
const queueManager = QueueManager.getInstance();
|
const queueManager = QueueManager.getInstance();
|
||||||
const queue = queueManager.getQueue(queueName);
|
queueManager.getQueue(queueName);
|
||||||
const batchSize = options.batchSize || 100;
|
const batchSize = options.batchSize || 100;
|
||||||
const batches = createBatches(items, batchSize);
|
const batches = createBatches(items, batchSize);
|
||||||
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
||||||
|
|
@ -166,7 +166,7 @@ export async function processBatchJob(
|
||||||
queueName: string
|
queueName: string
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const queueManager = QueueManager.getInstance();
|
const queueManager = QueueManager.getInstance();
|
||||||
const queue = queueManager.getQueue(queueName);
|
queueManager.getQueue(queueName);
|
||||||
const { payloadKey, batchIndex, totalBatches, itemCount } = jobData;
|
const { payloadKey, batchIndex, totalBatches, itemCount } = jobData;
|
||||||
|
|
||||||
logger.debug('Processing batch job', {
|
logger.debug('Processing batch job', {
|
||||||
|
|
@ -274,7 +274,7 @@ async function loadPayload<T>(
|
||||||
delayPerItem: number;
|
delayPerItem: number;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
retries: number;
|
retries: number;
|
||||||
provider: string;
|
handler: string;
|
||||||
operation: string;
|
operation: string;
|
||||||
};
|
};
|
||||||
} | null;
|
} | null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Queue, type Job } from 'bullmq';
|
import { Queue, type Job } from 'bullmq';
|
||||||
import { getLogger } from '@stock-bot/logger';
|
import { getLogger } from '@stock-bot/logger';
|
||||||
import type { JobData, DLQConfig, RedisConfig } from './types';
|
import type { DLQConfig, RedisConfig } from './types';
|
||||||
import { getRedisConnection } from './utils';
|
import { getRedisConnection } from './utils';
|
||||||
|
|
||||||
const logger = getLogger('dlq-handler');
|
const logger = getLogger('dlq-handler');
|
||||||
|
|
@ -12,7 +12,7 @@ export class DeadLetterQueueHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private mainQueue: Queue,
|
private mainQueue: Queue,
|
||||||
private connection: RedisConfig,
|
connection: RedisConfig,
|
||||||
config: DLQConfig = {}
|
config: DLQConfig = {}
|
||||||
) {
|
) {
|
||||||
this.config = {
|
this.config = {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ import type {
|
||||||
QueueOptions,
|
QueueOptions,
|
||||||
GlobalStats,
|
GlobalStats,
|
||||||
QueueStats,
|
QueueStats,
|
||||||
RateLimitRule,
|
RateLimitRule
|
||||||
RedisConfig
|
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getRedisConnection } from './utils';
|
import { getRedisConnection } from './utils';
|
||||||
|
|
||||||
|
|
@ -127,7 +126,7 @@ export class QueueManager {
|
||||||
const queueConfig: QueueWorkerConfig = {
|
const queueConfig: QueueWorkerConfig = {
|
||||||
workers: mergedOptions.workers,
|
workers: mergedOptions.workers,
|
||||||
concurrency: mergedOptions.concurrency,
|
concurrency: mergedOptions.concurrency,
|
||||||
startWorker: mergedOptions.workers && mergedOptions.workers > 0,
|
startWorker: !!mergedOptions.workers && mergedOptions.workers > 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const queue = new Queue(
|
const queue = new Queue(
|
||||||
|
|
@ -387,7 +386,7 @@ export class QueueManager {
|
||||||
try {
|
try {
|
||||||
await queue.close();
|
await queue.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Error closing queue', { error: error.message });
|
logger.warn('Error closing queue', { error: (error as Error).message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -396,16 +395,10 @@ export class QueueManager {
|
||||||
// Close all caches
|
// Close all caches
|
||||||
const cacheShutdownPromises = Array.from(this.caches.values()).map(async (cache) => {
|
const cacheShutdownPromises = Array.from(this.caches.values()).map(async (cache) => {
|
||||||
try {
|
try {
|
||||||
// Try different disconnect methods as different cache providers may use different names
|
// Clear cache before shutdown
|
||||||
if (typeof cache.disconnect === 'function') {
|
await cache.clear();
|
||||||
await cache.disconnect();
|
|
||||||
} else if (typeof cache.close === 'function') {
|
|
||||||
await cache.close();
|
|
||||||
} else if (typeof cache.quit === 'function') {
|
|
||||||
await cache.quit();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Error closing cache', { error: error.message });
|
logger.warn('Error clearing cache', { error: (error as Error).message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -417,7 +410,7 @@ export class QueueManager {
|
||||||
|
|
||||||
logger.info('QueueManager shutdown complete');
|
logger.info('QueueManager shutdown complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error during shutdown', { error: error.message });
|
logger.error('Error during shutdown', { error: (error as Error).message });
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Reset shutdown state
|
// Reset shutdown state
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Queue, QueueEvents } from 'bullmq';
|
||||||
import { getLogger } from '@stock-bot/logger';
|
import { getLogger } from '@stock-bot/logger';
|
||||||
import type { Job } from 'bullmq';
|
import type { Job } from 'bullmq';
|
||||||
|
|
||||||
const logger = getLogger('queue-metrics');
|
// const logger = getLogger('queue-metrics');
|
||||||
|
|
||||||
export interface QueueMetrics {
|
export interface QueueMetrics {
|
||||||
// Job counts
|
// Job counts
|
||||||
|
|
@ -55,20 +55,20 @@ export class QueueMetricsCollector {
|
||||||
* Setup event listeners for metrics collection
|
* Setup event listeners for metrics collection
|
||||||
*/
|
*/
|
||||||
private setupEventListeners(): void {
|
private setupEventListeners(): void {
|
||||||
this.queueEvents.on('completed', ({ jobId, returnvalue, prev }) => {
|
this.queueEvents.on('completed', () => {
|
||||||
// Record completion
|
// Record completion
|
||||||
this.completedTimestamps.push(Date.now());
|
this.completedTimestamps.push(Date.now());
|
||||||
this.cleanupOldTimestamps();
|
this.cleanupOldTimestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queueEvents.on('failed', ({ jobId, failedReason, prev }) => {
|
this.queueEvents.on('failed', () => {
|
||||||
// Record failure
|
// Record failure
|
||||||
this.failedTimestamps.push(Date.now());
|
this.failedTimestamps.push(Date.now());
|
||||||
this.cleanupOldTimestamps();
|
this.cleanupOldTimestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track processing times
|
// Track processing times
|
||||||
this.queueEvents.on('active', async ({ jobId, prev }) => {
|
this.queueEvents.on('active', async ({ jobId }) => {
|
||||||
const job = await this.getJob(jobId);
|
const job = await this.getJob(jobId);
|
||||||
if (job) {
|
if (job) {
|
||||||
(job as any)._startTime = Date.now();
|
(job as any)._startTime = Date.now();
|
||||||
|
|
@ -177,11 +177,11 @@ export class QueueMetricsCollector {
|
||||||
const sum = sorted.reduce((acc, val) => acc + val, 0);
|
const sum = sorted.reduce((acc, val) => acc + val, 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
avg: Math.round(sum / sorted.length),
|
avg: sorted.length > 0 ? Math.round(sum / sorted.length) : 0,
|
||||||
min: sorted[0],
|
min: sorted[0] || 0,
|
||||||
max: sorted[sorted.length - 1],
|
max: sorted[sorted.length - 1] || 0,
|
||||||
p95: sorted[Math.floor(sorted.length * 0.95)],
|
p95: sorted[Math.floor(sorted.length * 0.95)] || 0,
|
||||||
p99: sorted[Math.floor(sorted.length * 0.99)],
|
p99: sorted[Math.floor(sorted.length * 0.99)] || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,6 @@ export class Queue {
|
||||||
concurrency,
|
concurrency,
|
||||||
maxStalledCount: 3,
|
maxStalledCount: 3,
|
||||||
stalledInterval: 30000,
|
stalledInterval: 30000,
|
||||||
maxStalledTime: 60000,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export class QueueRateLimiter {
|
||||||
const key = this.getRuleKey(rule.level, rule.queueName, rule.handler, rule.operation);
|
const key = this.getRuleKey(rule.level, rule.queueName, rule.handler, rule.operation);
|
||||||
const limiter = new RateLimiterRedis({
|
const limiter = new RateLimiterRedis({
|
||||||
storeClient: this.redisClient,
|
storeClient: this.redisClient,
|
||||||
keyPrefix: rule.config.keyPrefix || `rl:${key}`,
|
keyPrefix: `rl:${key}`,
|
||||||
points: rule.config.points,
|
points: rule.config.points,
|
||||||
duration: rule.config.duration,
|
duration: rule.config.duration,
|
||||||
blockDuration: rule.config.blockDuration || 0,
|
blockDuration: rule.config.blockDuration || 0,
|
||||||
|
|
@ -224,7 +224,7 @@ export class QueueRateLimiter {
|
||||||
queueName,
|
queueName,
|
||||||
handler,
|
handler,
|
||||||
operation,
|
operation,
|
||||||
appliedRule,
|
appliedRule: applicableRule,
|
||||||
limit,
|
limit,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -233,7 +233,7 @@ export class QueueRateLimiter {
|
||||||
queueName,
|
queueName,
|
||||||
handler,
|
handler,
|
||||||
operation,
|
operation,
|
||||||
appliedRule,
|
appliedRule: applicableRule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,3 +154,19 @@ export interface DLQConfig {
|
||||||
alertThreshold?: number;
|
alertThreshold?: number;
|
||||||
cleanupAge?: number;
|
cleanupAge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DLQJobInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
failedReason: string;
|
||||||
|
attemptsMade: number;
|
||||||
|
timestamp: number;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleConfig {
|
||||||
|
pattern: string;
|
||||||
|
jobName: string;
|
||||||
|
data?: any;
|
||||||
|
options?: JobOptions;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { RedisConfig } from './types';
|
||||||
* Get Redis connection configuration with retry settings
|
* Get Redis connection configuration with retry settings
|
||||||
*/
|
*/
|
||||||
export function getRedisConnection(config: RedisConfig) {
|
export function getRedisConnection(config: RedisConfig) {
|
||||||
const isTest = process.env.NODE_ENV === 'test' || process.env.BUNIT === '1';
|
const isTest = process.env.NODE_ENV === 'test' || process.env['BUNIT'] === '1';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
host: config.host,
|
host: config.host,
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"test": "bun test",
|
|
||||||
"clean": "rimraf dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@stock-bot/event-bus": "*",
|
|
||||||
"@stock-bot/logger": "*",
|
|
||||||
"@stock-bot/utils": "*",
|
|
||||||
"commander": "^14.0.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import { EventBus } from '@stock-bot/event-bus';
|
|
||||||
import { getLogger } from '@stock-bot/logger';
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
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 = getLogger(`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.publish(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 = getLogger('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
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.lib.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"references": [{ "path": "../event-bus" }, { "path": "../logger" }, { "path": "../utils" }]
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"test": "bun test",
|
|
||||||
"clean": "rimraf dist"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@stock-bot/logger": "*",
|
|
||||||
"@stock-bot/utils": "*"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,397 +0,0 @@
|
||||||
// import { DataFrame } from '@stock-bot/data-frame';
|
|
||||||
// import { getLogger } from '@stock-bot/logger';
|
|
||||||
// import { atr, bollingerBands, ema, macd, rsi, sma } 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 = getLogger('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;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.lib.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"references": [{ "path": "../logger" }, { "path": "../utils" }]
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue