removed old services
This commit is contained in:
parent
f69c7b034b
commit
bc14acaeba
32 changed files with 0 additions and 2816 deletions
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"name": "@stock-bot/execution-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Execution service for stock trading bot - handles order execution and broker integration",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"devvvvv": "bun --watch src/index.ts",
|
||||
"start": "bun src/index.ts",
|
||||
"test": "bun test",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.12.0",
|
||||
"hono": "^4.6.1",
|
||||
"@stock-bot/config": "*",
|
||||
"@stock-bot/logger": "*",
|
||||
"@stock-bot/types": "*",
|
||||
"@stock-bot/event-bus": "*",
|
||||
"@stock-bot/utils": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"keywords": [
|
||||
"trading",
|
||||
"execution",
|
||||
"broker",
|
||||
"orders",
|
||||
"stock-bot"
|
||||
],
|
||||
"author": "Stock Bot Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { Order, OrderResult, OrderStatus } from '@stock-bot/types';
|
||||
|
||||
export interface BrokerInterface {
|
||||
/**
|
||||
* Execute an order with the broker
|
||||
*/
|
||||
executeOrder(order: Order): Promise<OrderResult>;
|
||||
|
||||
/**
|
||||
* Get order status from broker
|
||||
*/
|
||||
getOrderStatus(orderId: string): Promise<OrderStatus>;
|
||||
|
||||
/**
|
||||
* Cancel an order
|
||||
*/
|
||||
cancelOrder(orderId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get current positions
|
||||
*/
|
||||
getPositions(): Promise<Position[]>;
|
||||
|
||||
/**
|
||||
* Get account balance
|
||||
*/
|
||||
getAccountBalance(): Promise<AccountBalance>;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
unrealizedPnL: number;
|
||||
side: 'long' | 'short';
|
||||
}
|
||||
|
||||
export interface AccountBalance {
|
||||
totalValue: number;
|
||||
availableCash: number;
|
||||
buyingPower: number;
|
||||
marginUsed: number;
|
||||
}
|
||||
|
||||
export class MockBroker implements BrokerInterface {
|
||||
private orders: Map<string, OrderResult> = new Map();
|
||||
private positions: Position[] = [];
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
const orderId = `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const result: OrderResult = {
|
||||
orderId,
|
||||
symbol: order.symbol,
|
||||
quantity: order.quantity,
|
||||
side: order.side,
|
||||
status: 'filled',
|
||||
executedPrice: order.price || 100, // Mock price
|
||||
executedAt: new Date(),
|
||||
commission: 1.0,
|
||||
};
|
||||
|
||||
this.orders.set(orderId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getOrderStatus(orderId: string): Promise<OrderStatus> {
|
||||
const order = this.orders.get(orderId);
|
||||
return order?.status || 'unknown';
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: string): Promise<boolean> {
|
||||
const order = this.orders.get(orderId);
|
||||
if (order && order.status === 'pending') {
|
||||
order.status = 'cancelled';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getPositions(): Promise<Position[]> {
|
||||
return this.positions;
|
||||
}
|
||||
|
||||
async getAccountBalance(): Promise<AccountBalance> {
|
||||
return {
|
||||
totalValue: 100000,
|
||||
availableCash: 50000,
|
||||
buyingPower: 200000,
|
||||
marginUsed: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { logger } from '@stock-bot/logger';
|
||||
import { Order, OrderResult } from '@stock-bot/types';
|
||||
import { BrokerInterface } from '../broker/interface.ts';
|
||||
|
||||
export class OrderManager {
|
||||
private broker: BrokerInterface;
|
||||
private pendingOrders: Map<string, Order> = new Map();
|
||||
|
||||
constructor(broker: BrokerInterface) {
|
||||
this.broker = broker;
|
||||
}
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
try {
|
||||
logger.info(
|
||||
`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`
|
||||
);
|
||||
|
||||
// Add to pending orders
|
||||
const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.pendingOrders.set(orderId, order);
|
||||
|
||||
// Execute with broker
|
||||
const result = await this.broker.executeOrder(order);
|
||||
|
||||
// Remove from pending
|
||||
this.pendingOrders.delete(orderId);
|
||||
|
||||
logger.info(`Order executed successfully: ${result.orderId}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Order execution failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cancelOrder(orderId: string): Promise<boolean> {
|
||||
try {
|
||||
const success = await this.broker.cancelOrder(orderId);
|
||||
if (success) {
|
||||
this.pendingOrders.delete(orderId);
|
||||
logger.info(`Order cancelled: ${orderId}`);
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('Order cancellation failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrderStatus(orderId: string) {
|
||||
return await this.broker.getOrderStatus(orderId);
|
||||
}
|
||||
|
||||
getPendingOrders(): Order[] {
|
||||
return Array.from(this.pendingOrders.values());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { Order } from '@stock-bot/types';
|
||||
|
||||
export interface RiskRule {
|
||||
name: string;
|
||||
validate(order: Order, context: RiskContext): Promise<RiskValidationResult>;
|
||||
}
|
||||
|
||||
export interface RiskContext {
|
||||
currentPositions: Map<string, number>;
|
||||
accountBalance: number;
|
||||
totalExposure: number;
|
||||
maxPositionSize: number;
|
||||
maxDailyLoss: number;
|
||||
}
|
||||
|
||||
export interface RiskValidationResult {
|
||||
isValid: boolean;
|
||||
reason?: string;
|
||||
severity: 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
export class RiskManager {
|
||||
private logger = getLogger('risk-manager');
|
||||
private rules: RiskRule[] = [];
|
||||
|
||||
constructor() {
|
||||
this.initializeDefaultRules();
|
||||
}
|
||||
|
||||
addRule(rule: RiskRule): void {
|
||||
this.rules.push(rule);
|
||||
}
|
||||
|
||||
async validateOrder(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
for (const rule of this.rules) {
|
||||
const result = await rule.validate(order, context);
|
||||
if (!result.isValid) {
|
||||
logger.warn(`Risk rule violation: ${rule.name}`, {
|
||||
order,
|
||||
reason: result.reason,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
}
|
||||
|
||||
private initializeDefaultRules(): void {
|
||||
// Position size rule
|
||||
this.addRule({
|
||||
name: 'MaxPositionSize',
|
||||
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
const orderValue = order.quantity * (order.price || 0);
|
||||
|
||||
if (orderValue > context.maxPositionSize) {
|
||||
return {
|
||||
isValid: false,
|
||||
reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`,
|
||||
severity: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
},
|
||||
});
|
||||
|
||||
// Balance check rule
|
||||
this.addRule({
|
||||
name: 'SufficientBalance',
|
||||
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
const orderValue = order.quantity * (order.price || 0);
|
||||
|
||||
if (order.side === 'buy' && orderValue > context.accountBalance) {
|
||||
return {
|
||||
isValid: false,
|
||||
reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`,
|
||||
severity: 'error',
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
},
|
||||
});
|
||||
|
||||
// Concentration risk rule
|
||||
this.addRule({
|
||||
name: 'ConcentrationLimit',
|
||||
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
|
||||
const currentPosition = context.currentPositions.get(order.symbol) || 0;
|
||||
const newPosition =
|
||||
order.side === 'buy'
|
||||
? currentPosition + order.quantity
|
||||
: currentPosition - order.quantity;
|
||||
|
||||
const positionValue = Math.abs(newPosition) * (order.price || 0);
|
||||
const concentrationRatio = positionValue / context.accountBalance;
|
||||
|
||||
if (concentrationRatio > 0.25) {
|
||||
// 25% max concentration
|
||||
return {
|
||||
isValid: false,
|
||||
reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`,
|
||||
severity: 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, severity: 'info' };
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import { serve } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { config } from '@stock-bot/config';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
// import { BrokerInterface } from './broker/interface.ts';
|
||||
// import { OrderManager } from './execution/order-manager.ts';
|
||||
// import { RiskManager } from './execution/risk-manager.ts';
|
||||
|
||||
const app = new Hono();
|
||||
const logger = getLogger('execution-service');
|
||||
// Health check endpoint
|
||||
app.get('/health', c => {
|
||||
return c.json({
|
||||
status: 'healthy',
|
||||
service: 'execution-service',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// Order execution endpoints
|
||||
app.post('/orders/execute', async c => {
|
||||
try {
|
||||
const orderRequest = await c.req.json();
|
||||
logger.info('Received order execution request', orderRequest);
|
||||
|
||||
// TODO: Validate order and execute
|
||||
return c.json({
|
||||
orderId: `order_${Date.now()}`,
|
||||
status: 'pending',
|
||||
message: 'Order submitted for execution',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Order execution failed', error);
|
||||
return c.json({ error: 'Order execution failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/orders/:orderId/status', async c => {
|
||||
const orderId = c.req.param('orderId');
|
||||
|
||||
try {
|
||||
// TODO: Get order status from broker
|
||||
return c.json({
|
||||
orderId,
|
||||
status: 'filled',
|
||||
executedAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get order status', error);
|
||||
return c.json({ error: 'Failed to get order status' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/orders/:orderId/cancel', async c => {
|
||||
const orderId = c.req.param('orderId');
|
||||
|
||||
try {
|
||||
// TODO: Cancel order with broker
|
||||
return c.json({
|
||||
orderId,
|
||||
status: 'cancelled',
|
||||
cancelledAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to cancel order', error);
|
||||
return c.json({ error: 'Failed to cancel order' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Risk management endpoints
|
||||
app.get('/risk/position/:symbol', async c => {
|
||||
const symbol = c.req.param('symbol');
|
||||
|
||||
try {
|
||||
// TODO: Get position risk metrics
|
||||
return c.json({
|
||||
symbol,
|
||||
position: 100,
|
||||
exposure: 10000,
|
||||
risk: 'low',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get position risk', error);
|
||||
return c.json({ error: 'Failed to get position risk' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const port = config.EXECUTION_SERVICE_PORT || 3004;
|
||||
|
||||
logger.info(`Starting execution service on port ${port}`);
|
||||
|
||||
serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
},
|
||||
info => {
|
||||
logger.info(`Execution service is running on port ${info.port}`);
|
||||
}
|
||||
);
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__tests__/**"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../libs/types" },
|
||||
{ "path": "../../libs/config" },
|
||||
{ "path": "../../libs/logger" },
|
||||
{ "path": "../../libs/utils" },
|
||||
{ "path": "../../libs/event-bus" },
|
||||
{ "path": "../../libs/shutdown" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"@stock-bot/types#build",
|
||||
"@stock-bot/config#build",
|
||||
"@stock-bot/logger#build",
|
||||
"@stock-bot/utils#build",
|
||||
"@stock-bot/event-bus#build",
|
||||
"@stock-bot/shutdown#build"
|
||||
],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.spec.ts",
|
||||
"!**/test/**",
|
||||
"!**/tests/**",
|
||||
"!**/__tests__/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"name": "@stock-bot/portfolio-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Portfolio service for stock trading bot - handles portfolio tracking and performance analytics",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"devvvvv": "bun --watch src/index.ts",
|
||||
"start": "bun src/index.ts",
|
||||
"test": "bun test",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.12.0",
|
||||
"hono": "^4.6.1",
|
||||
"@stock-bot/config": "*",
|
||||
"@stock-bot/logger": "*",
|
||||
"@stock-bot/types": "*",
|
||||
"@stock-bot/questdb-client": "*",
|
||||
"@stock-bot/utils": "*",
|
||||
"@stock-bot/data-frame": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"keywords": [
|
||||
"trading",
|
||||
"portfolio",
|
||||
"performance",
|
||||
"analytics",
|
||||
"stock-bot"
|
||||
],
|
||||
"author": "Stock Bot Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
import { PortfolioSnapshot } from '../portfolio/portfolio-manager';
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
totalReturn: number;
|
||||
annualizedReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
volatility: number;
|
||||
beta: number;
|
||||
alpha: number;
|
||||
calmarRatio: number;
|
||||
sortinoRatio: number;
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
var95: number; // Value at Risk (95% confidence)
|
||||
cvar95: number; // Conditional Value at Risk
|
||||
maxDrawdown: number;
|
||||
downsideDeviation: number;
|
||||
correlationMatrix: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
export class PerformanceAnalyzer {
|
||||
private snapshots: PortfolioSnapshot[] = [];
|
||||
private benchmarkReturns: number[] = []; // S&P 500 or other benchmark
|
||||
|
||||
addSnapshot(snapshot: PortfolioSnapshot): void {
|
||||
this.snapshots.push(snapshot);
|
||||
// Keep only last 252 trading days (1 year)
|
||||
if (this.snapshots.length > 252) {
|
||||
this.snapshots = this.snapshots.slice(-252);
|
||||
}
|
||||
}
|
||||
|
||||
calculatePerformanceMetrics(
|
||||
period: 'daily' | 'weekly' | 'monthly' = 'daily'
|
||||
): PerformanceMetrics {
|
||||
if (this.snapshots.length < 2) {
|
||||
throw new Error('Need at least 2 snapshots to calculate performance');
|
||||
}
|
||||
|
||||
const returns = this.calculateReturns(period);
|
||||
const riskFreeRate = 0.02; // 2% annual risk-free rate
|
||||
|
||||
return {
|
||||
totalReturn: this.calculateTotalReturn(),
|
||||
annualizedReturn: this.calculateAnnualizedReturn(returns),
|
||||
sharpeRatio: this.calculateSharpeRatio(returns, riskFreeRate),
|
||||
maxDrawdown: this.calculateMaxDrawdown(),
|
||||
volatility: this.calculateVolatility(returns),
|
||||
beta: this.calculateBeta(returns),
|
||||
alpha: this.calculateAlpha(returns, riskFreeRate),
|
||||
calmarRatio: this.calculateCalmarRatio(returns),
|
||||
sortinoRatio: this.calculateSortinoRatio(returns, riskFreeRate),
|
||||
};
|
||||
}
|
||||
|
||||
calculateRiskMetrics(): RiskMetrics {
|
||||
const returns = this.calculateReturns('daily');
|
||||
|
||||
return {
|
||||
var95: this.calculateVaR(returns, 0.95),
|
||||
cvar95: this.calculateCVaR(returns, 0.95),
|
||||
maxDrawdown: this.calculateMaxDrawdown(),
|
||||
downsideDeviation: this.calculateDownsideDeviation(returns),
|
||||
correlationMatrix: {}, // TODO: Implement correlation matrix
|
||||
};
|
||||
}
|
||||
|
||||
private calculateReturns(_period: 'daily' | 'weekly' | 'monthly'): number[] {
|
||||
if (this.snapshots.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const returns: number[] = [];
|
||||
|
||||
for (let i = 1; i < this.snapshots.length; i++) {
|
||||
const currentValue = this.snapshots[i].totalValue;
|
||||
const previousValue = this.snapshots[i - 1].totalValue;
|
||||
const return_ = (currentValue - previousValue) / previousValue;
|
||||
returns.push(return_);
|
||||
}
|
||||
|
||||
return returns;
|
||||
}
|
||||
|
||||
private calculateTotalReturn(): number {
|
||||
if (this.snapshots.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const firstValue = this.snapshots[0].totalValue;
|
||||
const lastValue = this.snapshots[this.snapshots.length - 1].totalValue;
|
||||
|
||||
return (lastValue - firstValue) / firstValue;
|
||||
}
|
||||
|
||||
private calculateAnnualizedReturn(returns: number[]): number {
|
||||
if (returns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
return Math.pow(1 + avgReturn, 252) - 1; // 252 trading days per year
|
||||
}
|
||||
|
||||
private calculateVolatility(returns: number[]): number {
|
||||
if (returns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance =
|
||||
returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length;
|
||||
|
||||
return Math.sqrt(variance * 252); // Annualized volatility
|
||||
}
|
||||
|
||||
private calculateSharpeRatio(returns: number[], riskFreeRate: number): number {
|
||||
if (returns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const annualizedReturn = Math.pow(1 + avgReturn, 252) - 1;
|
||||
const volatility = this.calculateVolatility(returns);
|
||||
|
||||
if (volatility === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (annualizedReturn - riskFreeRate) / volatility;
|
||||
}
|
||||
|
||||
private calculateMaxDrawdown(): number {
|
||||
if (this.snapshots.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let maxDrawdown = 0;
|
||||
let peak = this.snapshots[0].totalValue;
|
||||
|
||||
for (const snapshot of this.snapshots) {
|
||||
if (snapshot.totalValue > peak) {
|
||||
peak = snapshot.totalValue;
|
||||
}
|
||||
|
||||
const drawdown = (peak - snapshot.totalValue) / peak;
|
||||
maxDrawdown = Math.max(maxDrawdown, drawdown);
|
||||
}
|
||||
|
||||
return maxDrawdown;
|
||||
}
|
||||
|
||||
private calculateBeta(returns: number[]): number {
|
||||
if (returns.length === 0 || this.benchmarkReturns.length === 0) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Simple beta calculation - would need actual benchmark data
|
||||
return 1.0; // Placeholder
|
||||
}
|
||||
|
||||
private calculateAlpha(returns: number[], riskFreeRate: number): number {
|
||||
const beta = this.calculateBeta(returns);
|
||||
const portfolioReturn = this.calculateAnnualizedReturn(returns);
|
||||
const benchmarkReturn = 0.1; // 10% benchmark return (placeholder)
|
||||
|
||||
return portfolioReturn - (riskFreeRate + beta * (benchmarkReturn - riskFreeRate));
|
||||
}
|
||||
|
||||
private calculateCalmarRatio(returns: number[]): number {
|
||||
const annualizedReturn = this.calculateAnnualizedReturn(returns);
|
||||
const maxDrawdown = this.calculateMaxDrawdown();
|
||||
|
||||
if (maxDrawdown === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return annualizedReturn / maxDrawdown;
|
||||
}
|
||||
|
||||
private calculateSortinoRatio(returns: number[], riskFreeRate: number): number {
|
||||
const annualizedReturn = this.calculateAnnualizedReturn(returns);
|
||||
const downsideDeviation = this.calculateDownsideDeviation(returns);
|
||||
|
||||
if (downsideDeviation === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (annualizedReturn - riskFreeRate) / downsideDeviation;
|
||||
}
|
||||
|
||||
private calculateDownsideDeviation(returns: number[]): number {
|
||||
if (returns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const negativeReturns = returns.filter(ret => ret < 0);
|
||||
if (negativeReturns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const avgNegativeReturn =
|
||||
negativeReturns.reduce((sum, ret) => sum + ret, 0) / negativeReturns.length;
|
||||
const variance =
|
||||
negativeReturns.reduce((sum, ret) => sum + Math.pow(ret - avgNegativeReturn, 2), 0) /
|
||||
negativeReturns.length;
|
||||
|
||||
return Math.sqrt(variance * 252); // Annualized
|
||||
}
|
||||
|
||||
private calculateVaR(returns: number[], confidence: number): number {
|
||||
if (returns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sortedReturns = returns.slice().sort((a, b) => a - b);
|
||||
const index = Math.floor((1 - confidence) * sortedReturns.length);
|
||||
|
||||
return -sortedReturns[index]; // Return as positive value
|
||||
}
|
||||
|
||||
private calculateCVaR(returns: number[], confidence: number): number {
|
||||
if (returns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sortedReturns = returns.slice().sort((a, b) => a - b);
|
||||
const cutoffIndex = Math.floor((1 - confidence) * sortedReturns.length);
|
||||
const tailReturns = sortedReturns.slice(0, cutoffIndex + 1);
|
||||
|
||||
if (tailReturns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const avgTailReturn = tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
|
||||
return -avgTailReturn; // Return as positive value
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import { serve } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { config } from '@stock-bot/config';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const app = new Hono();
|
||||
const logger = getLogger('portfolio-service');
|
||||
// Health check endpoint
|
||||
app.get('/health', c => {
|
||||
return c.json({
|
||||
status: 'healthy',
|
||||
service: 'portfolio-service',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// Portfolio endpoints
|
||||
app.get('/portfolio/overview', async c => {
|
||||
try {
|
||||
// TODO: Get portfolio overview
|
||||
return c.json({
|
||||
totalValue: 125000,
|
||||
totalReturn: 25000,
|
||||
totalReturnPercent: 25.0,
|
||||
dayChange: 1250,
|
||||
dayChangePercent: 1.0,
|
||||
positions: [],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get portfolio overview', error);
|
||||
return c.json({ error: 'Failed to get portfolio overview' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/portfolio/positions', async c => {
|
||||
try {
|
||||
// TODO: Get current positions
|
||||
return c.json([
|
||||
{
|
||||
symbol: 'AAPL',
|
||||
quantity: 100,
|
||||
averagePrice: 150.0,
|
||||
currentPrice: 155.0,
|
||||
marketValue: 15500,
|
||||
unrealizedPnL: 500,
|
||||
unrealizedPnLPercent: 3.33,
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error('Failed to get positions', error);
|
||||
return c.json({ error: 'Failed to get positions' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/portfolio/history', async c => {
|
||||
const days = c.req.query('days') || '30';
|
||||
|
||||
try {
|
||||
// TODO: Get portfolio history
|
||||
return c.json({
|
||||
period: `${days} days`,
|
||||
data: [],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get portfolio history', error);
|
||||
return c.json({ error: 'Failed to get portfolio history' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Performance analytics endpoints
|
||||
app.get('/analytics/performance', async c => {
|
||||
const period = c.req.query('period') || '1M';
|
||||
|
||||
try {
|
||||
// TODO: Calculate performance metrics
|
||||
return c.json({
|
||||
period,
|
||||
totalReturn: 0.25,
|
||||
annualizedReturn: 0.3,
|
||||
sharpeRatio: 1.5,
|
||||
maxDrawdown: 0.05,
|
||||
volatility: 0.15,
|
||||
beta: 1.1,
|
||||
alpha: 0.02,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get performance analytics', error);
|
||||
return c.json({ error: 'Failed to get performance analytics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/analytics/risk', async c => {
|
||||
try {
|
||||
// TODO: Calculate risk metrics
|
||||
return c.json({
|
||||
var95: 0.02,
|
||||
cvar95: 0.03,
|
||||
maxDrawdown: 0.05,
|
||||
downside_deviation: 0.08,
|
||||
correlation_matrix: {},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get risk analytics', error);
|
||||
return c.json({ error: 'Failed to get risk analytics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/analytics/attribution', async c => {
|
||||
try {
|
||||
// TODO: Calculate performance attribution
|
||||
return c.json({
|
||||
sector_allocation: {},
|
||||
security_selection: {},
|
||||
interaction_effect: {},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get attribution analytics', error);
|
||||
return c.json({ error: 'Failed to get attribution analytics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const port = config.PORTFOLIO_SERVICE_PORT || 3005;
|
||||
|
||||
logger.info(`Starting portfolio service on port ${port}`);
|
||||
|
||||
serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
},
|
||||
info => {
|
||||
logger.info(`Portfolio service is running on port ${info.port}`);
|
||||
}
|
||||
);
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
marketValue: number;
|
||||
unrealizedPnL: number;
|
||||
unrealizedPnLPercent: number;
|
||||
costBasis: number;
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
export interface PortfolioSnapshot {
|
||||
timestamp: Date;
|
||||
totalValue: number;
|
||||
cashBalance: number;
|
||||
positions: Position[];
|
||||
totalReturn: number;
|
||||
totalReturnPercent: number;
|
||||
dayChange: number;
|
||||
dayChangePercent: number;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id: string;
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
side: 'buy' | 'sell';
|
||||
timestamp: Date;
|
||||
commission: number;
|
||||
}
|
||||
|
||||
export class PortfolioManager {
|
||||
private logger = getLogger('PortfolioManager');
|
||||
private positions: Map<string, Position> = new Map();
|
||||
private trades: Trade[] = [];
|
||||
private cashBalance: number = 100000; // Starting cash
|
||||
|
||||
constructor(initialCash: number = 100000) {
|
||||
this.cashBalance = initialCash;
|
||||
}
|
||||
|
||||
addTrade(trade: Trade): void {
|
||||
this.trades.push(trade);
|
||||
this.updatePosition(trade);
|
||||
logger.info(`Trade added: ${trade.symbol} ${trade.side} ${trade.quantity} @ ${trade.price}`);
|
||||
}
|
||||
|
||||
private updatePosition(trade: Trade): void {
|
||||
const existing = this.positions.get(trade.symbol);
|
||||
|
||||
if (!existing) {
|
||||
// New position
|
||||
if (trade.side === 'buy') {
|
||||
this.positions.set(trade.symbol, {
|
||||
symbol: trade.symbol,
|
||||
quantity: trade.quantity,
|
||||
averagePrice: trade.price,
|
||||
currentPrice: trade.price,
|
||||
marketValue: trade.quantity * trade.price,
|
||||
unrealizedPnL: 0,
|
||||
unrealizedPnLPercent: 0,
|
||||
costBasis: trade.quantity * trade.price + trade.commission,
|
||||
lastUpdated: trade.timestamp,
|
||||
});
|
||||
this.cashBalance -= trade.quantity * trade.price + trade.commission;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing position
|
||||
if (trade.side === 'buy') {
|
||||
const newQuantity = existing.quantity + trade.quantity;
|
||||
const newCostBasis = existing.costBasis + trade.quantity * trade.price + trade.commission;
|
||||
|
||||
existing.quantity = newQuantity;
|
||||
existing.averagePrice = (newCostBasis - this.getTotalCommissions(trade.symbol)) / newQuantity;
|
||||
existing.costBasis = newCostBasis;
|
||||
existing.lastUpdated = trade.timestamp;
|
||||
|
||||
this.cashBalance -= trade.quantity * trade.price + trade.commission;
|
||||
} else if (trade.side === 'sell') {
|
||||
existing.quantity -= trade.quantity;
|
||||
existing.lastUpdated = trade.timestamp;
|
||||
|
||||
const proceeds = trade.quantity * trade.price - trade.commission;
|
||||
this.cashBalance += proceeds;
|
||||
|
||||
// Remove position if quantity is zero
|
||||
if (existing.quantity <= 0) {
|
||||
this.positions.delete(trade.symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePrice(symbol: string, price: number): void {
|
||||
const position = this.positions.get(symbol);
|
||||
if (position) {
|
||||
position.currentPrice = price;
|
||||
position.marketValue = position.quantity * price;
|
||||
position.unrealizedPnL = position.marketValue - position.quantity * position.averagePrice;
|
||||
position.unrealizedPnLPercent =
|
||||
(position.unrealizedPnL / (position.quantity * position.averagePrice)) * 100;
|
||||
position.lastUpdated = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
getPosition(symbol: string): Position | undefined {
|
||||
return this.positions.get(symbol);
|
||||
}
|
||||
|
||||
getAllPositions(): Position[] {
|
||||
return Array.from(this.positions.values());
|
||||
}
|
||||
|
||||
getPortfolioSnapshot(): PortfolioSnapshot {
|
||||
const positions = this.getAllPositions();
|
||||
const totalMarketValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0);
|
||||
const totalValue = totalMarketValue + this.cashBalance;
|
||||
const totalUnrealizedPnL = positions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0);
|
||||
|
||||
return {
|
||||
timestamp: new Date(),
|
||||
totalValue,
|
||||
cashBalance: this.cashBalance,
|
||||
positions,
|
||||
totalReturn: totalUnrealizedPnL, // Simplified - should include realized gains
|
||||
totalReturnPercent: (totalUnrealizedPnL / (totalValue - totalUnrealizedPnL)) * 100,
|
||||
dayChange: 0, // TODO: Calculate from previous day
|
||||
dayChangePercent: 0,
|
||||
};
|
||||
}
|
||||
|
||||
getTrades(symbol?: string): Trade[] {
|
||||
if (symbol) {
|
||||
return this.trades.filter(trade => trade.symbol === symbol);
|
||||
}
|
||||
return this.trades;
|
||||
}
|
||||
|
||||
private getTotalCommissions(symbol: string): number {
|
||||
return this.trades
|
||||
.filter(trade => trade.symbol === symbol)
|
||||
.reduce((sum, trade) => sum + trade.commission, 0);
|
||||
}
|
||||
|
||||
getCashBalance(): number {
|
||||
return this.cashBalance;
|
||||
}
|
||||
|
||||
getNetLiquidationValue(): number {
|
||||
const positions = this.getAllPositions();
|
||||
const positionValue = positions.reduce((sum, pos) => sum + pos.marketValue, 0);
|
||||
return positionValue + this.cashBalance;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__tests__/**"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../libs/types" },
|
||||
{ "path": "../../libs/config" },
|
||||
{ "path": "../../libs/logger" },
|
||||
{ "path": "../../libs/utils" },
|
||||
{ "path": "../../libs/postgres-client" },
|
||||
{ "path": "../../libs/event-bus" },
|
||||
{ "path": "../../libs/shutdown" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"@stock-bot/types#build",
|
||||
"@stock-bot/config#build",
|
||||
"@stock-bot/logger#build",
|
||||
"@stock-bot/utils#build",
|
||||
"@stock-bot/postgres-client#build",
|
||||
"@stock-bot/event-bus#build",
|
||||
"@stock-bot/shutdown#build"
|
||||
],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.spec.ts",
|
||||
"!**/test/**",
|
||||
"!**/tests/**",
|
||||
"!**/__tests__/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"name": "@stock-bot/processing-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Combined data processing and technical indicators service",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target node",
|
||||
"start": "bun dist/index.js",
|
||||
"test": "bun test",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/config": "*",
|
||||
"@stock-bot/logger": "*",
|
||||
"@stock-bot/shutdown": "*",
|
||||
"@stock-bot/types": "*",
|
||||
"@stock-bot/utils": "*",
|
||||
"@stock-bot/event-bus": "*",
|
||||
"@stock-bot/vector-engine": "*",
|
||||
"hono": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/**
|
||||
* Processing Service - Data processing and technical indicators service
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { getLogger, shutdownLoggers } from '@stock-bot/logger';
|
||||
import { Shutdown } from '@stock-bot/shutdown';
|
||||
import { healthRoutes, processingRoutes } from './routes';
|
||||
import { processingServiceManager } from './services';
|
||||
|
||||
// Load environment variables
|
||||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
const logger = getLogger('processing-service');
|
||||
const PORT = parseInt(process.env.PROCESSING_SERVICE_PORT || '3003');
|
||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||
|
||||
// Initialize shutdown manager with 15 second timeout
|
||||
const shutdown = Shutdown.getInstance({ timeout: 15000 });
|
||||
|
||||
// Register all routes
|
||||
app.route('', healthRoutes);
|
||||
app.route('/api/processing', processingRoutes);
|
||||
|
||||
// Initialize services
|
||||
async function initializeServices() {
|
||||
logger.info('Initializing processing service...');
|
||||
|
||||
try {
|
||||
// Initialize processing service manager
|
||||
logger.info('Starting processing service manager initialization...');
|
||||
await processingServiceManager.initialize();
|
||||
logger.info('Processing service manager initialized');
|
||||
|
||||
// TODO: Add other service initializations here as needed
|
||||
// - MongoDB client for reading/writing processed data
|
||||
// - Event bus for listening to data events
|
||||
// - Technical indicators engines
|
||||
// - Vector engines for similarity calculations
|
||||
|
||||
logger.info('All services initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize services', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Start server
|
||||
async function startServer() {
|
||||
await initializeServices();
|
||||
|
||||
// Start the HTTP server using Bun's native serve
|
||||
server = Bun.serve({
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
development: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
logger.info(`Processing Service started on port ${PORT}`);
|
||||
}
|
||||
|
||||
// Register shutdown handlers
|
||||
shutdown.onShutdown(async () => {
|
||||
if (server) {
|
||||
logger.info('Stopping HTTP server...');
|
||||
try {
|
||||
server.stop();
|
||||
logger.info('HTTP server stopped successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error stopping HTTP server', { error });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
shutdown.onShutdown(async () => {
|
||||
logger.info('Shutting down processing service manager...');
|
||||
try {
|
||||
await processingServiceManager.shutdown();
|
||||
logger.info('Processing service manager shut down successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error shutting down processing service manager', { error });
|
||||
}
|
||||
});
|
||||
|
||||
shutdown.onShutdown(async () => {
|
||||
logger.info('Shutting down loggers...');
|
||||
try {
|
||||
await shutdownLoggers();
|
||||
logger.info('Loggers shut down successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error shutting down loggers', { error });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions and unhandled rejections
|
||||
process.on('uncaughtException', error => {
|
||||
logger.error('Uncaught exception', { error });
|
||||
shutdown.shutdownAndExit('uncaughtException', 1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled rejection', { reason, promise });
|
||||
shutdown.shutdownAndExit('unhandledRejection', 1);
|
||||
});
|
||||
|
||||
// Handle shutdown signals
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('Received SIGINT signal');
|
||||
shutdown.shutdownAndExit('SIGINT', 0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('Received SIGTERM signal');
|
||||
shutdown.shutdownAndExit('SIGTERM', 0);
|
||||
});
|
||||
|
||||
// Start the service
|
||||
startServer().catch(error => {
|
||||
logger.error('Failed to start processing service', { error });
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/**
|
||||
* Health and status routes for processing service
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const healthRoutes = new Hono();
|
||||
|
||||
// Health check endpoint
|
||||
healthRoutes.get('/health', c => {
|
||||
return c.json({
|
||||
status: 'healthy',
|
||||
service: 'processing-service',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
});
|
||||
});
|
||||
|
||||
// Detailed status endpoint
|
||||
healthRoutes.get('/status', c => {
|
||||
return c.json({
|
||||
service: 'processing-service',
|
||||
status: 'running',
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
});
|
||||
});
|
||||
|
||||
// Ready check endpoint
|
||||
healthRoutes.get('/ready', c => {
|
||||
// TODO: Add checks for service dependencies
|
||||
// - Database connections
|
||||
// - Event bus connections
|
||||
// - Required resources
|
||||
|
||||
return c.json({
|
||||
status: 'ready',
|
||||
service: 'processing-service',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
export { healthRoutes };
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* Route exports for processing service
|
||||
*/
|
||||
export { healthRoutes } from './health.routes';
|
||||
export { processingRoutes } from './processing.routes';
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* Processing routes for data processing operations
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { processingServiceManager } from '../services';
|
||||
|
||||
const processingRoutes = new Hono();
|
||||
const logger = getLogger('processing-routes');
|
||||
|
||||
// Process data endpoint
|
||||
processingRoutes.post('/process', async c => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
|
||||
logger.info('Processing request received', {
|
||||
dataType: body.type,
|
||||
recordCount: body.data?.length || 0,
|
||||
});
|
||||
|
||||
// Use processing service manager to handle the request
|
||||
const result = await processingServiceManager.processData(
|
||||
body.type || 'unknown',
|
||||
body.data || []
|
||||
);
|
||||
|
||||
return c.json({
|
||||
status: 'success',
|
||||
message: 'Data processing completed',
|
||||
result,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Processing error', { error });
|
||||
return c.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: 'Processing failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get processing status
|
||||
processingRoutes.get('/status', c => {
|
||||
const status = processingServiceManager.getStatus();
|
||||
return c.json({
|
||||
...status,
|
||||
activeJobs: 0, // TODO: Implement job tracking
|
||||
queueLength: 0, // TODO: Implement queue monitoring
|
||||
lastProcessed: null, // TODO: Track last processing time
|
||||
});
|
||||
});
|
||||
|
||||
export { processingRoutes };
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* Services exports for processing service
|
||||
*/
|
||||
export { ProcessingServiceManager, processingServiceManager } from './processing.service';
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
/**
|
||||
* Processing Service Manager
|
||||
*
|
||||
* Manages the core processing operations for the processing service
|
||||
*/
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('processing-service-manager');
|
||||
|
||||
export class ProcessingServiceManager {
|
||||
private isInitialized = false;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isInitialized) {
|
||||
logger.warn('Processing service manager already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Initializing processing service manager...');
|
||||
|
||||
try {
|
||||
// TODO: Initialize processing components
|
||||
// - Technical indicators engine
|
||||
// - Data transformation pipeline
|
||||
// - Event listeners for data events
|
||||
// - Job queues for processing tasks
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info('Processing service manager initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize processing service manager', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (!this.isInitialized) {
|
||||
logger.warn('Processing service manager not initialized, nothing to shutdown');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Shutting down processing service manager...');
|
||||
|
||||
try {
|
||||
// TODO: Cleanup processing components
|
||||
// - Stop job processing
|
||||
// - Close database connections
|
||||
// - Cleanup event listeners
|
||||
|
||||
this.isInitialized = false;
|
||||
logger.info('Processing service manager shut down successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error shutting down processing service manager', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data with technical indicators
|
||||
*/
|
||||
async processData(
|
||||
dataType: string,
|
||||
data: unknown[]
|
||||
): Promise<{
|
||||
status: string;
|
||||
dataType: string;
|
||||
inputCount: number;
|
||||
outputCount: number;
|
||||
processedAt: Date;
|
||||
processingTime: number;
|
||||
}> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Processing service manager not initialized');
|
||||
}
|
||||
|
||||
logger.info(`Processing ${data.length} records of type: ${dataType}`);
|
||||
|
||||
try {
|
||||
// TODO: Implement actual processing logic
|
||||
// - Apply technical indicators
|
||||
// - Calculate signals
|
||||
// - Transform data format
|
||||
// - Save processed results
|
||||
|
||||
const result = {
|
||||
status: 'success',
|
||||
dataType,
|
||||
inputCount: data.length,
|
||||
outputCount: data.length, // Placeholder
|
||||
processedAt: new Date(),
|
||||
processingTime: 0, // Placeholder
|
||||
};
|
||||
|
||||
logger.info('Data processing completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Data processing failed', { error, dataType, inputCount: data.length });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processing service status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
initialized: this.isInitialized,
|
||||
status: this.isInitialized ? 'ready' : 'not_initialized',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const processingServiceManager = new ProcessingServiceManager();
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__tests__/**"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../libs/types" },
|
||||
{ "path": "../../libs/config" },
|
||||
{ "path": "../../libs/logger" },
|
||||
{ "path": "../../libs/utils" },
|
||||
{ "path": "../../libs/data-frame" },
|
||||
{ "path": "../../libs/vector-engine" },
|
||||
{ "path": "../../libs/mongodb-client" },
|
||||
{ "path": "../../libs/event-bus" },
|
||||
{ "path": "../../libs/shutdown" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"@stock-bot/types#build",
|
||||
"@stock-bot/config#build",
|
||||
"@stock-bot/logger#build",
|
||||
"@stock-bot/utils#build",
|
||||
"@stock-bot/data-frame#build",
|
||||
"@stock-bot/vector-engine#build",
|
||||
"@stock-bot/mongodb-client#build",
|
||||
"@stock-bot/event-bus#build",
|
||||
"@stock-bot/shutdown#build"
|
||||
],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.spec.ts",
|
||||
"!**/test/**",
|
||||
"!**/tests/**",
|
||||
"!**/__tests__/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"name": "@stock-bot/strategy-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Combined strategy execution and multi-mode backtesting service",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"devvvvv": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target node",
|
||||
"start": "bun dist/index.js",
|
||||
"test": "bun test",
|
||||
"clean": "rm -rf dist",
|
||||
"backtest": "bun src/cli/index.ts",
|
||||
"optimize": "bun src/cli/index.ts optimize",
|
||||
"cli": "bun src/cli/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/config": "*",
|
||||
"@stock-bot/logger": "*",
|
||||
"@stock-bot/types": "*",
|
||||
"@stock-bot/utils": "*",
|
||||
"@stock-bot/event-bus": "*",
|
||||
"@stock-bot/strategy-engine": "*",
|
||||
"@stock-bot/vector-engine": "*",
|
||||
"@stock-bot/data-frame": "*",
|
||||
"@stock-bot/questdb-client": "*",
|
||||
"hono": "^4.0.0",
|
||||
"commander": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/**
|
||||
* Event-Driven Backtesting Mode
|
||||
* Processes data point by point with realistic order execution
|
||||
*/
|
||||
import { ExecutionMode, MarketData, Order, OrderResult } from '../../framework/execution-mode';
|
||||
|
||||
export interface BacktestConfig {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
initialCapital: number;
|
||||
slippageModel?: string;
|
||||
commissionModel?: string;
|
||||
}
|
||||
|
||||
export class EventMode extends ExecutionMode {
|
||||
name = 'event-driven';
|
||||
private simulationTime: Date;
|
||||
private historicalData: Map<string, MarketData[]> = new Map();
|
||||
|
||||
constructor(private config: BacktestConfig) {
|
||||
super();
|
||||
this.simulationTime = config.startDate;
|
||||
}
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
this.logger.debug('Simulating order execution', {
|
||||
orderId: order.id,
|
||||
simulationTime: this.simulationTime,
|
||||
});
|
||||
|
||||
// TODO: Implement realistic order simulation
|
||||
// Include slippage, commission, market impact
|
||||
const simulatedResult: OrderResult = {
|
||||
orderId: order.id,
|
||||
symbol: order.symbol,
|
||||
executedQuantity: order.quantity,
|
||||
executedPrice: 100, // TODO: Get realistic price
|
||||
commission: 1.0, // TODO: Calculate based on commission model
|
||||
slippage: 0.01, // TODO: Calculate based on slippage model
|
||||
timestamp: this.simulationTime,
|
||||
executionTime: 50, // ms
|
||||
};
|
||||
|
||||
return simulatedResult;
|
||||
}
|
||||
|
||||
getCurrentTime(): Date {
|
||||
return this.simulationTime;
|
||||
}
|
||||
|
||||
async getMarketData(symbol: string): Promise<MarketData> {
|
||||
const data = this.historicalData.get(symbol) || [];
|
||||
const currentData = data.find(d => d.timestamp <= this.simulationTime);
|
||||
|
||||
if (!currentData) {
|
||||
throw new Error(`No market data available for ${symbol} at ${this.simulationTime}`);
|
||||
}
|
||||
|
||||
return currentData;
|
||||
}
|
||||
|
||||
async publishEvent(event: string, data: any): Promise<void> {
|
||||
// In-memory event bus for simulation
|
||||
this.logger.debug('Publishing simulation event', { event, data });
|
||||
}
|
||||
|
||||
// Simulation control methods
|
||||
advanceTime(newTime: Date): void {
|
||||
this.simulationTime = newTime;
|
||||
}
|
||||
|
||||
loadHistoricalData(symbol: string, data: MarketData[]): void {
|
||||
this.historicalData.set(symbol, data);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,424 +0,0 @@
|
|||
import { DataFrame } from '@stock-bot/data-frame';
|
||||
import { EventBus } from '@stock-bot/event-bus';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
|
||||
import { BacktestContext, BacktestResult, ExecutionMode } from '../framework/execution-mode';
|
||||
import { EventMode } from './event-mode';
|
||||
import VectorizedMode from './vectorized-mode';
|
||||
|
||||
export interface HybridModeConfig {
|
||||
vectorizedThreshold: number; // Switch to vectorized if data points > threshold
|
||||
warmupPeriod: number; // Number of periods for initial vectorized calculation
|
||||
eventDrivenRealtime: boolean; // Use event-driven for real-time portions
|
||||
optimizeIndicators: boolean; // Pre-calculate indicators vectorized
|
||||
batchSize: number; // Size of batches for hybrid processing
|
||||
}
|
||||
|
||||
export class HybridMode extends ExecutionMode {
|
||||
private vectorEngine: VectorEngine;
|
||||
private eventMode: EventMode;
|
||||
private vectorizedMode: VectorizedMode;
|
||||
private config: HybridModeConfig;
|
||||
private precomputedIndicators: Map<string, number[]> = new Map();
|
||||
private currentIndex: number = 0;
|
||||
|
||||
constructor(context: BacktestContext, eventBus: EventBus, config: HybridModeConfig = {}) {
|
||||
super(context, eventBus);
|
||||
|
||||
this.config = {
|
||||
vectorizedThreshold: 50000,
|
||||
warmupPeriod: 1000,
|
||||
eventDrivenRealtime: true,
|
||||
optimizeIndicators: true,
|
||||
batchSize: 10000,
|
||||
...config,
|
||||
};
|
||||
|
||||
this.vectorEngine = new VectorEngine();
|
||||
this.eventMode = new EventMode(context, eventBus);
|
||||
this.vectorizedMode = new VectorizedMode(context, eventBus);
|
||||
|
||||
this.logger = getLogger('hybrid-mode');
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
|
||||
// Initialize both modes
|
||||
await this.eventMode.initialize();
|
||||
await this.vectorizedMode.initialize();
|
||||
|
||||
this.logger.info('Hybrid mode initialized', {
|
||||
backtestId: this.context.backtestId,
|
||||
config: this.config,
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<BacktestResult> {
|
||||
const startTime = Date.now();
|
||||
this.logger.info('Starting hybrid backtest execution');
|
||||
|
||||
try {
|
||||
// Determine execution strategy based on data size
|
||||
const dataSize = await this.estimateDataSize();
|
||||
|
||||
if (dataSize <= this.config.vectorizedThreshold) {
|
||||
// Small dataset: use pure vectorized approach
|
||||
this.logger.info('Using pure vectorized approach for small dataset', { dataSize });
|
||||
return await this.vectorizedMode.execute();
|
||||
}
|
||||
|
||||
// Large dataset: use hybrid approach
|
||||
this.logger.info('Using hybrid approach for large dataset', { dataSize });
|
||||
return await this.executeHybrid(startTime);
|
||||
} catch (error) {
|
||||
this.logger.error('Hybrid backtest failed', {
|
||||
error,
|
||||
backtestId: this.context.backtestId,
|
||||
});
|
||||
|
||||
await this.eventBus.publishBacktestUpdate(this.context.backtestId, 0, {
|
||||
status: 'failed',
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeHybrid(startTime: number): Promise<BacktestResult> {
|
||||
// Phase 1: Vectorized warmup and indicator pre-computation
|
||||
const warmupResult = await this.executeWarmupPhase();
|
||||
|
||||
// Phase 2: Event-driven processing with pre-computed indicators
|
||||
const eventResult = await this.executeEventPhase(warmupResult);
|
||||
|
||||
// Phase 3: Combine results
|
||||
const combinedResult = this.combineResults(warmupResult, eventResult, startTime);
|
||||
|
||||
await this.eventBus.publishBacktestUpdate(this.context.backtestId, 100, {
|
||||
status: 'completed',
|
||||
result: combinedResult,
|
||||
});
|
||||
|
||||
this.logger.info('Hybrid backtest completed', {
|
||||
backtestId: this.context.backtestId,
|
||||
duration: Date.now() - startTime,
|
||||
totalTrades: combinedResult.trades.length,
|
||||
warmupTrades: warmupResult.trades.length,
|
||||
eventTrades: eventResult.trades.length,
|
||||
});
|
||||
|
||||
return combinedResult;
|
||||
}
|
||||
|
||||
private async executeWarmupPhase(): Promise<BacktestResult> {
|
||||
this.logger.info('Executing vectorized warmup phase', {
|
||||
warmupPeriod: this.config.warmupPeriod,
|
||||
});
|
||||
|
||||
// Load warmup data
|
||||
const warmupData = await this.loadWarmupData();
|
||||
const dataFrame = this.createDataFrame(warmupData);
|
||||
|
||||
// Pre-compute indicators for entire dataset if optimization is enabled
|
||||
if (this.config.optimizeIndicators) {
|
||||
await this.precomputeIndicators(dataFrame);
|
||||
}
|
||||
|
||||
// Run vectorized backtest on warmup period
|
||||
const strategyCode = this.generateStrategyCode();
|
||||
const vectorResult = await this.vectorEngine.executeVectorizedStrategy(
|
||||
dataFrame.head(this.config.warmupPeriod),
|
||||
strategyCode
|
||||
);
|
||||
|
||||
// Convert to standard format
|
||||
return this.convertVectorizedResult(vectorResult, Date.now());
|
||||
}
|
||||
|
||||
private async executeEventPhase(warmupResult: BacktestResult): Promise<BacktestResult> {
|
||||
this.logger.info('Executing event-driven phase');
|
||||
|
||||
// Set up event mode with warmup context
|
||||
this.currentIndex = this.config.warmupPeriod;
|
||||
|
||||
// Create modified context for event phase
|
||||
const eventContext: BacktestContext = {
|
||||
...this.context,
|
||||
initialPortfolio: this.extractFinalPortfolio(warmupResult),
|
||||
};
|
||||
|
||||
// Execute event-driven backtest for remaining data
|
||||
const eventMode = new EventMode(eventContext, this.eventBus);
|
||||
await eventMode.initialize();
|
||||
|
||||
// Override indicator calculations to use pre-computed values
|
||||
if (this.config.optimizeIndicators) {
|
||||
this.overrideIndicatorCalculations(eventMode);
|
||||
}
|
||||
|
||||
return await eventMode.execute();
|
||||
}
|
||||
|
||||
private async precomputeIndicators(dataFrame: DataFrame): Promise<void> {
|
||||
this.logger.info('Pre-computing indicators vectorized');
|
||||
|
||||
const close = dataFrame.getColumn('close');
|
||||
const high = dataFrame.getColumn('high');
|
||||
const low = dataFrame.getColumn('low');
|
||||
|
||||
// Import technical indicators from vector engine
|
||||
const { TechnicalIndicators } = await import('@stock-bot/vector-engine');
|
||||
|
||||
// Pre-compute common indicators
|
||||
this.precomputedIndicators.set('sma_20', TechnicalIndicators.sma(close, 20));
|
||||
this.precomputedIndicators.set('sma_50', TechnicalIndicators.sma(close, 50));
|
||||
this.precomputedIndicators.set('ema_12', TechnicalIndicators.ema(close, 12));
|
||||
this.precomputedIndicators.set('ema_26', TechnicalIndicators.ema(close, 26));
|
||||
this.precomputedIndicators.set('rsi', TechnicalIndicators.rsi(close));
|
||||
this.precomputedIndicators.set('atr', TechnicalIndicators.atr(high, low, close));
|
||||
|
||||
const macd = TechnicalIndicators.macd(close);
|
||||
this.precomputedIndicators.set('macd', macd.macd);
|
||||
this.precomputedIndicators.set('macd_signal', macd.signal);
|
||||
this.precomputedIndicators.set('macd_histogram', macd.histogram);
|
||||
|
||||
const bb = TechnicalIndicators.bollingerBands(close);
|
||||
this.precomputedIndicators.set('bb_upper', bb.upper);
|
||||
this.precomputedIndicators.set('bb_middle', bb.middle);
|
||||
this.precomputedIndicators.set('bb_lower', bb.lower);
|
||||
|
||||
this.logger.info('Indicators pre-computed', {
|
||||
indicators: Array.from(this.precomputedIndicators.keys()),
|
||||
});
|
||||
}
|
||||
|
||||
private overrideIndicatorCalculations(eventMode: EventMode): void {
|
||||
// Override the event mode's indicator calculations to use pre-computed values
|
||||
// This is a simplified approach - in production you'd want a more sophisticated interface
|
||||
const _originalCalculateIndicators = (eventMode as any).calculateIndicators;
|
||||
|
||||
(eventMode as any).calculateIndicators = (symbol: string, index: number) => {
|
||||
const indicators: Record<string, number> = {};
|
||||
|
||||
for (const [name, values] of this.precomputedIndicators.entries()) {
|
||||
if (index < values.length) {
|
||||
indicators[name] = values[index];
|
||||
}
|
||||
}
|
||||
|
||||
return indicators;
|
||||
};
|
||||
}
|
||||
|
||||
private async estimateDataSize(): Promise<number> {
|
||||
// Estimate the number of data points for the backtest period
|
||||
const startTime = new Date(this.context.startDate).getTime();
|
||||
const endTime = new Date(this.context.endDate).getTime();
|
||||
const timeRange = endTime - startTime;
|
||||
|
||||
// Assume 1-minute intervals (60000ms)
|
||||
const estimatedPoints = Math.floor(timeRange / 60000);
|
||||
|
||||
this.logger.debug('Estimated data size', {
|
||||
timeRange,
|
||||
estimatedPoints,
|
||||
threshold: this.config.vectorizedThreshold,
|
||||
});
|
||||
|
||||
return estimatedPoints;
|
||||
}
|
||||
|
||||
private async loadWarmupData(): Promise<any[]> {
|
||||
// Load historical data for warmup phase
|
||||
// This should load more data than just the warmup period for indicator calculations
|
||||
const data = [];
|
||||
const startTime = new Date(this.context.startDate).getTime();
|
||||
const warmupEndTime = startTime + this.config.warmupPeriod * 60000;
|
||||
|
||||
// Add extra lookback for indicator calculations
|
||||
const lookbackTime = startTime - 200 * 60000; // 200 periods lookback
|
||||
|
||||
for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) {
|
||||
const basePrice = 100 + Math.sin(timestamp / 1000000) * 10;
|
||||
const volatility = 0.02;
|
||||
|
||||
const open = basePrice + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const close = open + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const high = Math.max(open, close) + Math.random() * volatility * basePrice;
|
||||
const low = Math.min(open, close) - Math.random() * volatility * basePrice;
|
||||
const volume = Math.floor(Math.random() * 10000) + 1000;
|
||||
|
||||
data.push({
|
||||
timestamp,
|
||||
symbol: this.context.symbol,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private createDataFrame(data: any[]): DataFrame {
|
||||
return new DataFrame(data, {
|
||||
columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'],
|
||||
dtypes: {
|
||||
timestamp: 'number',
|
||||
symbol: 'string',
|
||||
open: 'number',
|
||||
high: 'number',
|
||||
low: 'number',
|
||||
close: 'number',
|
||||
volume: 'number',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private generateStrategyCode(): string {
|
||||
// Generate strategy code based on context
|
||||
const strategy = this.context.strategy;
|
||||
|
||||
if (strategy.type === 'sma_crossover') {
|
||||
return 'sma_crossover';
|
||||
}
|
||||
|
||||
return strategy.code || 'sma_crossover';
|
||||
}
|
||||
|
||||
private convertVectorizedResult(
|
||||
vectorResult: VectorizedBacktestResult,
|
||||
startTime: number
|
||||
): BacktestResult {
|
||||
return {
|
||||
backtestId: this.context.backtestId,
|
||||
strategy: this.context.strategy,
|
||||
symbol: this.context.symbol,
|
||||
startDate: this.context.startDate,
|
||||
endDate: this.context.endDate,
|
||||
mode: 'hybrid-vectorized',
|
||||
duration: Date.now() - startTime,
|
||||
trades: vectorResult.trades.map(trade => ({
|
||||
id: `trade_${trade.entryIndex}_${trade.exitIndex}`,
|
||||
symbol: this.context.symbol,
|
||||
side: trade.side,
|
||||
entryTime: vectorResult.timestamps[trade.entryIndex],
|
||||
exitTime: vectorResult.timestamps[trade.exitIndex],
|
||||
entryPrice: trade.entryPrice,
|
||||
exitPrice: trade.exitPrice,
|
||||
quantity: trade.quantity,
|
||||
pnl: trade.pnl,
|
||||
commission: 0,
|
||||
slippage: 0,
|
||||
})),
|
||||
performance: {
|
||||
totalReturn: vectorResult.metrics.totalReturns,
|
||||
sharpeRatio: vectorResult.metrics.sharpeRatio,
|
||||
maxDrawdown: vectorResult.metrics.maxDrawdown,
|
||||
winRate: vectorResult.metrics.winRate,
|
||||
profitFactor: vectorResult.metrics.profitFactor,
|
||||
totalTrades: vectorResult.metrics.totalTrades,
|
||||
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
|
||||
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
|
||||
avgTrade: vectorResult.metrics.avgTrade,
|
||||
avgWin:
|
||||
vectorResult.trades.filter(t => t.pnl > 0).reduce((sum, t) => sum + t.pnl, 0) /
|
||||
vectorResult.trades.filter(t => t.pnl > 0).length || 0,
|
||||
avgLoss:
|
||||
vectorResult.trades.filter(t => t.pnl <= 0).reduce((sum, t) => sum + t.pnl, 0) /
|
||||
vectorResult.trades.filter(t => t.pnl <= 0).length || 0,
|
||||
largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0),
|
||||
},
|
||||
equity: vectorResult.equity,
|
||||
drawdown: vectorResult.metrics.drawdown,
|
||||
metadata: {
|
||||
mode: 'hybrid-vectorized',
|
||||
dataPoints: vectorResult.timestamps.length,
|
||||
signals: Object.keys(vectorResult.signals),
|
||||
optimizations: ['vectorized_warmup', 'precomputed_indicators'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private extractFinalPortfolio(warmupResult: BacktestResult): any {
|
||||
// Extract the final portfolio state from warmup phase
|
||||
const finalEquity = warmupResult.equity[warmupResult.equity.length - 1] || 10000;
|
||||
|
||||
return {
|
||||
cash: finalEquity,
|
||||
positions: [], // Simplified - in production would track actual positions
|
||||
equity: finalEquity,
|
||||
};
|
||||
}
|
||||
|
||||
private combineResults(
|
||||
warmupResult: BacktestResult,
|
||||
eventResult: BacktestResult,
|
||||
startTime: number
|
||||
): BacktestResult {
|
||||
// Combine results from both phases
|
||||
const combinedTrades = [...warmupResult.trades, ...eventResult.trades];
|
||||
const combinedEquity = [...warmupResult.equity, ...eventResult.equity];
|
||||
const combinedDrawdown = [...(warmupResult.drawdown || []), ...(eventResult.drawdown || [])];
|
||||
|
||||
// Recalculate combined performance metrics
|
||||
const totalPnL = combinedTrades.reduce((sum, trade) => sum + trade.pnl, 0);
|
||||
const winningTrades = combinedTrades.filter(t => t.pnl > 0);
|
||||
const losingTrades = combinedTrades.filter(t => t.pnl <= 0);
|
||||
|
||||
const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
|
||||
const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
|
||||
|
||||
return {
|
||||
backtestId: this.context.backtestId,
|
||||
strategy: this.context.strategy,
|
||||
symbol: this.context.symbol,
|
||||
startDate: this.context.startDate,
|
||||
endDate: this.context.endDate,
|
||||
mode: 'hybrid',
|
||||
duration: Date.now() - startTime,
|
||||
trades: combinedTrades,
|
||||
performance: {
|
||||
totalReturn:
|
||||
(combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0],
|
||||
sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation
|
||||
maxDrawdown: Math.max(...combinedDrawdown),
|
||||
winRate: winningTrades.length / combinedTrades.length,
|
||||
profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity,
|
||||
totalTrades: combinedTrades.length,
|
||||
winningTrades: winningTrades.length,
|
||||
losingTrades: losingTrades.length,
|
||||
avgTrade: totalPnL / combinedTrades.length,
|
||||
avgWin: grossProfit / winningTrades.length || 0,
|
||||
avgLoss: grossLoss / losingTrades.length || 0,
|
||||
largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0),
|
||||
},
|
||||
equity: combinedEquity,
|
||||
drawdown: combinedDrawdown,
|
||||
metadata: {
|
||||
mode: 'hybrid',
|
||||
phases: ['vectorized-warmup', 'event-driven'],
|
||||
warmupPeriod: this.config.warmupPeriod,
|
||||
optimizations: ['precomputed_indicators', 'hybrid_execution'],
|
||||
warmupTrades: warmupResult.trades.length,
|
||||
eventTrades: eventResult.trades.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await super.cleanup();
|
||||
await this.eventMode.cleanup();
|
||||
await this.vectorizedMode.cleanup();
|
||||
this.precomputedIndicators.clear();
|
||||
this.logger.info('Hybrid mode cleanup completed');
|
||||
}
|
||||
}
|
||||
|
||||
export default HybridMode;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* Live Trading Mode
|
||||
* Executes orders through real brokers
|
||||
*/
|
||||
import { ExecutionMode, MarketData, Order, OrderResult } from '../../framework/execution-mode';
|
||||
|
||||
export class LiveMode extends ExecutionMode {
|
||||
name = 'live';
|
||||
|
||||
async executeOrder(order: Order): Promise<OrderResult> {
|
||||
this.logger.info('Executing live order', { orderId: order.id });
|
||||
|
||||
// TODO: Implement real broker integration
|
||||
// This will connect to actual brokerage APIs
|
||||
throw new Error('Live broker integration not implemented yet');
|
||||
}
|
||||
|
||||
getCurrentTime(): Date {
|
||||
return new Date(); // Real time
|
||||
}
|
||||
|
||||
async getMarketData(_symbol: string): Promise<MarketData> {
|
||||
// TODO: Get live market data
|
||||
throw new Error('Live market data fetching not implemented yet');
|
||||
}
|
||||
|
||||
async publishEvent(event: string, data: any): Promise<void> {
|
||||
// TODO: Publish to real event bus (Dragonfly)
|
||||
this.logger.debug('Publishing event', { event, data });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
import { DataFrame } from '@stock-bot/data-frame';
|
||||
import { EventBus } from '@stock-bot/event-bus';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
|
||||
import { BacktestContext, BacktestResult, ExecutionMode } from '../framework/execution-mode';
|
||||
|
||||
export interface VectorizedModeConfig {
|
||||
batchSize?: number;
|
||||
enableOptimization?: boolean;
|
||||
parallelProcessing?: boolean;
|
||||
}
|
||||
|
||||
export class VectorizedMode extends ExecutionMode {
|
||||
private vectorEngine: VectorEngine;
|
||||
private config: VectorizedModeConfig;
|
||||
private logger = getLogger('vectorized-mode');
|
||||
|
||||
constructor(context: BacktestContext, eventBus: EventBus, config: VectorizedModeConfig = {}) {
|
||||
super(context, eventBus);
|
||||
this.vectorEngine = new VectorEngine();
|
||||
this.config = {
|
||||
batchSize: 10000,
|
||||
enableOptimization: true,
|
||||
parallelProcessing: true,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await super.initialize();
|
||||
this.logger.info('Vectorized mode initialized', {
|
||||
backtestId: this.context.backtestId,
|
||||
config: this.config,
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<BacktestResult> {
|
||||
const startTime = Date.now();
|
||||
this.logger.info('Starting vectorized backtest execution');
|
||||
|
||||
try {
|
||||
// Load all data at once for vectorized processing
|
||||
const data = await this.loadHistoricalData();
|
||||
|
||||
// Convert to DataFrame format
|
||||
const dataFrame = this.createDataFrame(data);
|
||||
|
||||
// Execute vectorized strategy
|
||||
const strategyCode = this.generateStrategyCode();
|
||||
const vectorResult = await this.vectorEngine.executeVectorizedStrategy(
|
||||
dataFrame,
|
||||
strategyCode
|
||||
);
|
||||
|
||||
// Convert to standard backtest result format
|
||||
const result = this.convertVectorizedResult(vectorResult, startTime);
|
||||
|
||||
// Emit completion event
|
||||
await this.eventBus.publishBacktestUpdate(this.context.backtestId, 100, {
|
||||
status: 'completed',
|
||||
result,
|
||||
});
|
||||
|
||||
this.logger.info('Vectorized backtest completed', {
|
||||
backtestId: this.context.backtestId,
|
||||
duration: Date.now() - startTime,
|
||||
totalTrades: result.trades.length,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Vectorized backtest failed', {
|
||||
error,
|
||||
backtestId: this.context.backtestId,
|
||||
});
|
||||
|
||||
await this.eventBus.publishBacktestUpdate(this.context.backtestId, 0, {
|
||||
status: 'failed',
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadHistoricalData(): Promise<any[]> {
|
||||
// Load all historical data at once
|
||||
// This is much more efficient than loading tick by tick
|
||||
const data = [];
|
||||
|
||||
// Simulate loading data (in production, this would be a bulk database query)
|
||||
const startTime = new Date(this.context.startDate).getTime();
|
||||
const endTime = new Date(this.context.endDate).getTime();
|
||||
const interval = 60000; // 1 minute intervals
|
||||
|
||||
for (let timestamp = startTime; timestamp <= endTime; timestamp += interval) {
|
||||
// Simulate OHLCV data
|
||||
const basePrice = 100 + Math.sin(timestamp / 1000000) * 10;
|
||||
const volatility = 0.02;
|
||||
|
||||
const open = basePrice + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const close = open + (Math.random() - 0.5) * volatility * basePrice;
|
||||
const high = Math.max(open, close) + Math.random() * volatility * basePrice;
|
||||
const low = Math.min(open, close) - Math.random() * volatility * basePrice;
|
||||
const volume = Math.floor(Math.random() * 10000) + 1000;
|
||||
|
||||
data.push({
|
||||
timestamp,
|
||||
symbol: this.context.symbol,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private createDataFrame(data: any[]): DataFrame {
|
||||
return new DataFrame(data, {
|
||||
columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'],
|
||||
dtypes: {
|
||||
timestamp: 'number',
|
||||
symbol: 'string',
|
||||
open: 'number',
|
||||
high: 'number',
|
||||
low: 'number',
|
||||
close: 'number',
|
||||
volume: 'number',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private generateStrategyCode(): string {
|
||||
// Convert strategy configuration to vectorized strategy code
|
||||
// This is a simplified example - in production you'd have a more sophisticated compiler
|
||||
const strategy = this.context.strategy;
|
||||
|
||||
if (strategy.type === 'sma_crossover') {
|
||||
return 'sma_crossover';
|
||||
}
|
||||
|
||||
// Add more strategy types as needed
|
||||
return strategy.code || 'sma_crossover';
|
||||
}
|
||||
|
||||
private convertVectorizedResult(
|
||||
vectorResult: VectorizedBacktestResult,
|
||||
startTime: number
|
||||
): BacktestResult {
|
||||
return {
|
||||
backtestId: this.context.backtestId,
|
||||
strategy: this.context.strategy,
|
||||
symbol: this.context.symbol,
|
||||
startDate: this.context.startDate,
|
||||
endDate: this.context.endDate,
|
||||
mode: 'vectorized',
|
||||
duration: Date.now() - startTime,
|
||||
trades: vectorResult.trades.map(trade => ({
|
||||
id: `trade_${trade.entryIndex}_${trade.exitIndex}`,
|
||||
symbol: this.context.symbol,
|
||||
side: trade.side,
|
||||
entryTime: vectorResult.timestamps[trade.entryIndex],
|
||||
exitTime: vectorResult.timestamps[trade.exitIndex],
|
||||
entryPrice: trade.entryPrice,
|
||||
exitPrice: trade.exitPrice,
|
||||
quantity: trade.quantity,
|
||||
pnl: trade.pnl,
|
||||
commission: 0, // Simplified
|
||||
slippage: 0,
|
||||
})),
|
||||
performance: {
|
||||
totalReturn: vectorResult.metrics.totalReturns,
|
||||
sharpeRatio: vectorResult.metrics.sharpeRatio,
|
||||
maxDrawdown: vectorResult.metrics.maxDrawdown,
|
||||
winRate: vectorResult.metrics.winRate,
|
||||
profitFactor: vectorResult.metrics.profitFactor,
|
||||
totalTrades: vectorResult.metrics.totalTrades,
|
||||
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
|
||||
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
|
||||
avgTrade: vectorResult.metrics.avgTrade,
|
||||
avgWin:
|
||||
vectorResult.trades.filter(t => t.pnl > 0).reduce((sum, t) => sum + t.pnl, 0) /
|
||||
vectorResult.trades.filter(t => t.pnl > 0).length || 0,
|
||||
avgLoss:
|
||||
vectorResult.trades.filter(t => t.pnl <= 0).reduce((sum, t) => sum + t.pnl, 0) /
|
||||
vectorResult.trades.filter(t => t.pnl <= 0).length || 0,
|
||||
largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
|
||||
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0),
|
||||
},
|
||||
equity: vectorResult.equity,
|
||||
drawdown: vectorResult.metrics.drawdown,
|
||||
metadata: {
|
||||
mode: 'vectorized',
|
||||
dataPoints: vectorResult.timestamps.length,
|
||||
signals: Object.keys(vectorResult.signals),
|
||||
optimizations: this.config.enableOptimization ? ['vectorized_computation'] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await super.cleanup();
|
||||
this.logger.info('Vectorized mode cleanup completed');
|
||||
}
|
||||
|
||||
// Batch processing capabilities
|
||||
async batchBacktest(
|
||||
strategies: Array<{ id: string; config: any }>
|
||||
): Promise<Record<string, BacktestResult>> {
|
||||
this.logger.info('Starting batch vectorized backtest', {
|
||||
strategiesCount: strategies.length,
|
||||
});
|
||||
|
||||
const data = await this.loadHistoricalData();
|
||||
const dataFrame = this.createDataFrame(data);
|
||||
|
||||
const strategyConfigs = strategies.map(s => ({
|
||||
id: s.id,
|
||||
code: this.generateStrategyCode(),
|
||||
}));
|
||||
|
||||
const batchResults = await this.vectorEngine.batchBacktest(dataFrame, strategyConfigs);
|
||||
const results: Record<string, BacktestResult> = {};
|
||||
|
||||
for (const [strategyId, vectorResult] of Object.entries(batchResults)) {
|
||||
results[strategyId] = this.convertVectorizedResult(vectorResult, Date.now());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export default VectorizedMode;
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Strategy Service CLI
|
||||
* Command-line interface for running backtests and managing strategies
|
||||
*/
|
||||
import { program } from 'commander';
|
||||
import { createEventBus } from '@stock-bot/event-bus';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { EventMode } from '../backtesting/modes/event-mode';
|
||||
import HybridMode from '../backtesting/modes/hybrid-mode';
|
||||
import { LiveMode } from '../backtesting/modes/live-mode';
|
||||
import VectorizedMode from '../backtesting/modes/vectorized-mode';
|
||||
import { BacktestContext } from '../framework/execution-mode';
|
||||
|
||||
const logger = getLogger('strategy-cli');
|
||||
|
||||
interface CLIBacktestConfig {
|
||||
strategy: string;
|
||||
strategies: string;
|
||||
symbol: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
mode: 'live' | 'event' | 'vectorized' | 'hybrid';
|
||||
initialCapital?: number;
|
||||
config?: string;
|
||||
output?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
async function runBacktest(options: CLIBacktestConfig): Promise<void> {
|
||||
logger.info('Starting backtest from CLI', { options });
|
||||
|
||||
try {
|
||||
// Initialize event bus
|
||||
const eventBus = createEventBus({
|
||||
serviceName: 'strategy-cli',
|
||||
enablePersistence: false, // Disable Redis for CLI
|
||||
});
|
||||
|
||||
// Create backtest context
|
||||
const context: BacktestContext = {
|
||||
backtestId: `cli_${Date.now()}`,
|
||||
strategy: {
|
||||
id: options.strategy,
|
||||
name: options.strategy,
|
||||
type: options.strategy,
|
||||
code: options.strategy,
|
||||
parameters: {},
|
||||
},
|
||||
symbol: options.symbol,
|
||||
startDate: options.startDate,
|
||||
endDate: options.endDate,
|
||||
initialCapital: options.initialCapital || 10000,
|
||||
mode: options.mode,
|
||||
};
|
||||
|
||||
// Load additional config if provided
|
||||
if (options.config) {
|
||||
const configData = await loadConfig(options.config);
|
||||
context.strategy.parameters = { ...context.strategy.parameters, ...configData };
|
||||
}
|
||||
|
||||
// Create and execute the appropriate mode
|
||||
let executionMode;
|
||||
|
||||
switch (options.mode) {
|
||||
case 'live':
|
||||
executionMode = new LiveMode(context, eventBus);
|
||||
break;
|
||||
case 'event':
|
||||
executionMode = new EventMode(context, eventBus);
|
||||
break;
|
||||
case 'vectorized':
|
||||
executionMode = new VectorizedMode(context, eventBus);
|
||||
break;
|
||||
case 'hybrid':
|
||||
executionMode = new HybridMode(context, eventBus);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown execution mode: ${options.mode}`);
|
||||
}
|
||||
|
||||
// Subscribe to progress updates
|
||||
eventBus.subscribe('backtest.update', message => {
|
||||
const { backtestId: _backtestId, progress, ...data } = message.data;
|
||||
console.log(`Progress: ${progress}%`, data);
|
||||
});
|
||||
|
||||
await executionMode.initialize();
|
||||
const result = await executionMode.execute();
|
||||
await executionMode.cleanup();
|
||||
|
||||
// Display results
|
||||
displayResults(result);
|
||||
|
||||
// Save results if output specified
|
||||
if (options.output) {
|
||||
await saveResults(result, options.output);
|
||||
}
|
||||
|
||||
await eventBus.close();
|
||||
} catch (error) {
|
||||
logger.error('Backtest failed', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig(configPath: string): Promise<any> {
|
||||
try {
|
||||
if (configPath.endsWith('.json')) {
|
||||
const file = Bun.file(configPath);
|
||||
return await file.json();
|
||||
} else {
|
||||
// Assume it's a JavaScript/TypeScript module
|
||||
return await import(configPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load config', { configPath, error });
|
||||
throw new Error(`Failed to load config from ${configPath}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(result: any): void {
|
||||
console.log('\n=== Backtest Results ===');
|
||||
console.log(`Strategy: ${result.strategy.name}`);
|
||||
console.log(`Symbol: ${result.symbol}`);
|
||||
console.log(`Period: ${result.startDate} to ${result.endDate}`);
|
||||
console.log(`Mode: ${result.mode}`);
|
||||
console.log(`Duration: ${result.duration}ms`);
|
||||
|
||||
console.log('\n--- Performance ---');
|
||||
console.log(`Total Return: ${(result.performance.totalReturn * 100).toFixed(2)}%`);
|
||||
console.log(`Sharpe Ratio: ${result.performance.sharpeRatio.toFixed(3)}`);
|
||||
console.log(`Max Drawdown: ${(result.performance.maxDrawdown * 100).toFixed(2)}%`);
|
||||
console.log(`Win Rate: ${(result.performance.winRate * 100).toFixed(1)}%`);
|
||||
console.log(`Profit Factor: ${result.performance.profitFactor.toFixed(2)}`);
|
||||
|
||||
console.log('\n--- Trading Stats ---');
|
||||
console.log(`Total Trades: ${result.performance.totalTrades}`);
|
||||
console.log(`Winning Trades: ${result.performance.winningTrades}`);
|
||||
console.log(`Losing Trades: ${result.performance.losingTrades}`);
|
||||
console.log(`Average Trade: ${result.performance.avgTrade.toFixed(2)}`);
|
||||
console.log(`Average Win: ${result.performance.avgWin.toFixed(2)}`);
|
||||
console.log(`Average Loss: ${result.performance.avgLoss.toFixed(2)}`);
|
||||
console.log(`Largest Win: ${result.performance.largestWin.toFixed(2)}`);
|
||||
console.log(`Largest Loss: ${result.performance.largestLoss.toFixed(2)}`);
|
||||
|
||||
if (result.metadata) {
|
||||
console.log('\n--- Metadata ---');
|
||||
Object.entries(result.metadata).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${Array.isArray(value) ? value.join(', ') : value}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveResults(result: any, outputPath: string): Promise<void> {
|
||||
try {
|
||||
if (outputPath.endsWith('.json')) {
|
||||
await Bun.write(outputPath, JSON.stringify(result, null, 2));
|
||||
} else if (outputPath.endsWith('.csv')) {
|
||||
const csv = convertTradesToCSV(result.trades);
|
||||
await Bun.write(outputPath, csv);
|
||||
} else {
|
||||
// Default to JSON
|
||||
await Bun.write(outputPath + '.json', JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
logger.info(`\nResults saved to: ${outputPath}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to save results', { outputPath, error });
|
||||
}
|
||||
}
|
||||
|
||||
function convertTradesToCSV(trades: any[]): string {
|
||||
if (trades.length === 0) {
|
||||
return 'No trades executed\n';
|
||||
}
|
||||
|
||||
const headers = Object.keys(trades[0]).join(',');
|
||||
const rows = trades.map(trade =>
|
||||
Object.values(trade)
|
||||
.map(value => (typeof value === 'string' ? `"${value}"` : value))
|
||||
.join(',')
|
||||
);
|
||||
|
||||
return [headers, ...rows].join('\n');
|
||||
}
|
||||
|
||||
async function listStrategies(): Promise<void> {
|
||||
console.log('Available strategies:');
|
||||
console.log(' sma_crossover - Simple Moving Average Crossover');
|
||||
console.log(' ema_crossover - Exponential Moving Average Crossover');
|
||||
console.log(' rsi_mean_reversion - RSI Mean Reversion');
|
||||
console.log(' macd_trend - MACD Trend Following');
|
||||
console.log(' bollinger_bands - Bollinger Bands Strategy');
|
||||
// Add more as they're implemented
|
||||
}
|
||||
|
||||
async function validateStrategy(strategy: string): Promise<void> {
|
||||
console.log(`Validating strategy: ${strategy}`);
|
||||
|
||||
// TODO: Add strategy validation logic
|
||||
// This could check if the strategy exists, has valid parameters, etc.
|
||||
|
||||
const validStrategies = [
|
||||
'sma_crossover',
|
||||
'ema_crossover',
|
||||
'rsi_mean_reversion',
|
||||
'macd_trend',
|
||||
'bollinger_bands',
|
||||
];
|
||||
|
||||
if (!validStrategies.includes(strategy)) {
|
||||
console.warn(`Warning: Strategy '${strategy}' is not in the list of known strategies`);
|
||||
console.log('Use --list-strategies to see available strategies');
|
||||
} else {
|
||||
console.log(`✓ Strategy '${strategy}' is valid`);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI Commands
|
||||
program.name('strategy-cli').description('Stock Trading Bot Strategy CLI').version('1.0.0');
|
||||
|
||||
program
|
||||
.command('backtest')
|
||||
.description('Run a backtest')
|
||||
.requiredOption('-s, --strategy <strategy>', 'Strategy to test')
|
||||
.requiredOption('--symbol <symbol>', 'Symbol to trade')
|
||||
.requiredOption('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
||||
.requiredOption('--end-date <date>', 'End date (YYYY-MM-DD)')
|
||||
.option('-m, --mode <mode>', 'Execution mode', 'vectorized')
|
||||
.option('-c, --initial-capital <amount>', 'Initial capital', '10000')
|
||||
.option('--config <path>', 'Configuration file path')
|
||||
.option('-o, --output <path>', 'Output file path')
|
||||
.option('-v, --verbose', 'Verbose output')
|
||||
.action(async (options: CLIBacktestConfig) => {
|
||||
await runBacktest(options);
|
||||
});
|
||||
|
||||
program.command('list-strategies').description('List available strategies').action(listStrategies);
|
||||
|
||||
program
|
||||
.command('validate')
|
||||
.description('Validate a strategy')
|
||||
.requiredOption('-s, --strategy <strategy>', 'Strategy to validate')
|
||||
.action(async (options: CLIBacktestConfig) => {
|
||||
await validateStrategy(options.strategy);
|
||||
});
|
||||
|
||||
program
|
||||
.command('compare')
|
||||
.description('Compare multiple strategies')
|
||||
.requiredOption('--strategies <strategies>', 'Comma-separated list of strategies')
|
||||
.requiredOption('--symbol <symbol>', 'Symbol to trade')
|
||||
.requiredOption('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
||||
.requiredOption('--end-date <date>', 'End date (YYYY-MM-DD)')
|
||||
.option('-m, --mode <mode>', 'Execution mode', 'vectorized')
|
||||
.option('-c, --initial-capital <amount>', 'Initial capital', '10000')
|
||||
.option('-o, --output <path>', 'Output directory')
|
||||
.action(async (options: CLIBacktestConfig) => {
|
||||
const strategies = options.strategies.split(',').map((s: string) => s.trim());
|
||||
console.log(`Comparing strategies: ${strategies.join(', ')}`);
|
||||
|
||||
const _results: any[] = [];
|
||||
|
||||
for (const strategy of strategies) {
|
||||
console.log(`\nRunning ${strategy}...`);
|
||||
try {
|
||||
await runBacktest({
|
||||
...options,
|
||||
strategy,
|
||||
output: options.output ? `${options.output}/${strategy}.json` : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to run ${strategy}:`, (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nComparison completed!');
|
||||
});
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse();
|
||||
|
||||
export { listStrategies, runBacktest, validateStrategy };
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/**
|
||||
* Execution Mode Framework
|
||||
* Base classes for different execution modes (live, event-driven, vectorized)
|
||||
*/
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const _logger = getLogger('execution-mode');
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: 'BUY' | 'SELL';
|
||||
quantity: number;
|
||||
type: 'MARKET' | 'LIMIT';
|
||||
price?: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface OrderResult {
|
||||
orderId: string;
|
||||
symbol: string;
|
||||
executedQuantity: number;
|
||||
executedPrice: number;
|
||||
commission: number;
|
||||
slippage: number;
|
||||
timestamp: Date;
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
export interface MarketData {
|
||||
symbol: string;
|
||||
timestamp: Date;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export abstract class ExecutionMode {
|
||||
protected logger = getLogger(this.constructor.name);
|
||||
|
||||
abstract name: string;
|
||||
abstract executeOrder(order: Order): Promise<OrderResult>;
|
||||
abstract getCurrentTime(): Date;
|
||||
abstract getMarketData(symbol: string): Promise<MarketData>;
|
||||
abstract publishEvent(event: string, data: any): Promise<void>;
|
||||
}
|
||||
|
||||
export enum BacktestMode {
|
||||
LIVE = 'live',
|
||||
EVENT_DRIVEN = 'event-driven',
|
||||
VECTORIZED = 'vectorized',
|
||||
HYBRID = 'hybrid',
|
||||
}
|
||||
|
||||
export class ModeFactory {
|
||||
static create(mode: BacktestMode, _config?: any): ExecutionMode {
|
||||
switch (mode) {
|
||||
case BacktestMode.LIVE:
|
||||
// TODO: Import and create LiveMode
|
||||
throw new Error('LiveMode not implemented yet');
|
||||
|
||||
case BacktestMode.EVENT_DRIVEN:
|
||||
// TODO: Import and create EventBacktestMode
|
||||
throw new Error('EventBacktestMode not implemented yet');
|
||||
|
||||
case BacktestMode.VECTORIZED:
|
||||
// TODO: Import and create VectorBacktestMode
|
||||
throw new Error('VectorBacktestMode not implemented yet');
|
||||
|
||||
case BacktestMode.HYBRID:
|
||||
// TODO: Import and create HybridBacktestMode
|
||||
throw new Error('HybridBacktestMode not implemented yet');
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown mode: ${mode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
/**
|
||||
* Strategy Service - Multi-mode strategy execution and backtesting
|
||||
*/
|
||||
import { serve } from '@hono/node-server';
|
||||
import { Hono } from 'hono';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
// Load environment variables
|
||||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
const logger = getLogger('strategy-service');
|
||||
const PORT = parseInt(process.env.STRATEGY_SERVICE_PORT || '3004');
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', c => {
|
||||
return c.json({
|
||||
service: 'strategy-service',
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// Strategy execution endpoints
|
||||
app.post('/api/strategy/run', async c => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Strategy run request', {
|
||||
strategy: body.strategy,
|
||||
mode: body.mode,
|
||||
});
|
||||
|
||||
// TODO: Implement strategy execution
|
||||
return c.json({
|
||||
message: 'Strategy execution endpoint - not implemented yet',
|
||||
strategy: body.strategy,
|
||||
mode: body.mode,
|
||||
});
|
||||
});
|
||||
|
||||
// Backtesting endpoints
|
||||
app.post('/api/backtest/event', async c => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Event-driven backtest request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement event-driven backtesting
|
||||
return c.json({
|
||||
message: 'Event-driven backtest endpoint - not implemented yet',
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/backtest/vector', async c => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Vectorized backtest request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement vectorized backtesting
|
||||
return c.json({
|
||||
message: 'Vectorized backtest endpoint - not implemented yet',
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/backtest/hybrid', async c => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Hybrid backtest request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement hybrid backtesting
|
||||
return c.json({
|
||||
message: 'Hybrid backtest endpoint - not implemented yet',
|
||||
});
|
||||
});
|
||||
|
||||
// Parameter optimization endpoint
|
||||
app.post('/api/optimize', async c => {
|
||||
const body = await c.req.json();
|
||||
logger.info('Parameter optimization request', { strategy: body.strategy });
|
||||
|
||||
// TODO: Implement parameter optimization
|
||||
return c.json({
|
||||
message: 'Parameter optimization endpoint - not implemented yet',
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: PORT,
|
||||
});
|
||||
|
||||
logger.info(`Strategy Service started on port ${PORT}`);
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__tests__/**"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../libs/types" },
|
||||
{ "path": "../../libs/config" },
|
||||
{ "path": "../../libs/logger" },
|
||||
{ "path": "../../libs/utils" },
|
||||
{ "path": "../../libs/strategy-engine" },
|
||||
{ "path": "../../libs/event-bus" },
|
||||
{ "path": "../../libs/shutdown" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"@stock-bot/types#build",
|
||||
"@stock-bot/config#build",
|
||||
"@stock-bot/logger#build",
|
||||
"@stock-bot/utils#build",
|
||||
"@stock-bot/strategy-engine#build",
|
||||
"@stock-bot/event-bus#build",
|
||||
"@stock-bot/shutdown#build"
|
||||
],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.spec.ts",
|
||||
"!**/test/**",
|
||||
"!**/tests/**",
|
||||
"!**/__tests__/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue