running prettier for cleanup

This commit is contained in:
Boki 2025-06-11 10:13:25 -04:00
parent fe7733aeb5
commit d85cd58acd
151 changed files with 29158 additions and 27966 deletions

32
.githooks/pre-commit Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Pre-commit hook to run Prettier
echo "Running Prettier format check..."
# Check if prettier is available
if ! command -v prettier &> /dev/null; then
echo "Prettier not found. Please install it with: bun add -d prettier"
exit 1
fi
# Run prettier check on staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|js|json)$')
if [[ -n "$STAGED_FILES" ]]; then
echo "Checking format for staged files..."
# Check if files are formatted
npx prettier --check $STAGED_FILES
if [[ $? -ne 0 ]]; then
echo ""
echo "❌ Some files are not formatted correctly."
echo "Please run 'npm run format' or 'bun run format' to fix formatting issues."
echo "Or run 'npx prettier --write $STAGED_FILES' to format just the staged files."
exit 1
fi
echo "✅ All staged files are properly formatted."
fi
exit 0

110
.prettierignore Normal file
View file

@ -0,0 +1,110 @@
# Dependencies
node_modules/
**/node_modules/
# Build outputs
dist/
build/
**/dist/
**/build/
.next/
**/.next/
# Cache directories
.turbo/
**/.turbo/
.cache/
**/.cache/
# Environment files
.env
.env.local
.env.production
.env.staging
**/.env*
# Lock files
package-lock.json
yarn.lock
bun.lockb
pnpm-lock.yaml
bun.lock
# Logs
*.log
logs/
**/logs/
# Database files
*.db
*.sqlite
*.sqlite3
# Temporary files
*.tmp
*.temp
.DS_Store
Thumbs.db
# IDE/Editor files
.vscode/settings.json
.idea/
*.swp
*.swo
# Angular specific
**/.angular/
# Test coverage
coverage/
**/coverage/
# Generated documentation
docs/generated/
**/docs/generated/
# Docker
Dockerfile*
docker-compose*.yml
# Scripts (might have different formatting requirements)
scripts/
**/scripts/
# Configuration files that should maintain their format
*.md
*.yml
*.yaml
*.toml
!package.json
!tsconfig*.json
!.prettierrc
# Git files
.gitignore
.dockerignore
# Binary and special files
*.ico
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.woff
*.woff2
*.ttf
*.eot
# SQL files
*.sql
# Shell scripts
*.sh
*.bat
*.ps1
# Config files that need special formatting
bunfig.toml
angular.json
turbo.json

26
.prettierrc Normal file
View file

@ -0,0 +1,26 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": false,
"quoteProps": "as-needed",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
"^(node:.*|fs|path|crypto|url|os|util|events|stream|buffer|child_process|cluster|http|https|net|tls|dgram|dns|readline|repl|vm|zlib|querystring|punycode|assert|timers|constants)$",
"<THIRD_PARTY_MODULES>",
"^@stock-bot/(.*)$",
"^@/(.*)$",
"^\\.\\.(?!/?$)",
"^\\.\\./?$",
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
"importOrderParserPlugins": ["typescript", "decorators-legacy"]
}

21
.vscode/settings.json vendored
View file

@ -20,5 +20,24 @@
"yaml.validate": true, "yaml.validate": true,
"yaml.completion": true, "yaml.completion": true,
"yaml.hover": true, "yaml.hover": true,
"yaml.format.enable": true "yaml.format.enable": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
} }

View file

@ -1,8 +1,11 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
@ -11,6 +14,6 @@ export const appConfig: ApplicationConfig = {
provideZonelessChangeDetection(), provideZonelessChangeDetection(),
provideRouter(routes), provideRouter(routes),
provideHttpClient(), provideHttpClient(),
provideAnimationsAsync() provideAnimationsAsync(),
] ],
}; };

View file

@ -2,9 +2,9 @@ import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component'; import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { MarketDataComponent } from './pages/market-data/market-data.component'; import { MarketDataComponent } from './pages/market-data/market-data.component';
import { PortfolioComponent } from './pages/portfolio/portfolio.component'; import { PortfolioComponent } from './pages/portfolio/portfolio.component';
import { StrategiesComponent } from './pages/strategies/strategies.component';
import { RiskManagementComponent } from './pages/risk-management/risk-management.component'; import { RiskManagementComponent } from './pages/risk-management/risk-management.component';
import { SettingsComponent } from './pages/settings/settings.component'; import { SettingsComponent } from './pages/settings/settings.component';
import { StrategiesComponent } from './pages/strategies/strategies.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
@ -14,5 +14,5 @@ export const routes: Routes = [
{ path: 'strategies', component: StrategiesComponent }, { path: 'strategies', component: StrategiesComponent },
{ path: 'risk-management', component: RiskManagementComponent }, { path: 'risk-management', component: RiskManagementComponent },
{ path: 'settings', component: SettingsComponent }, { path: 'settings', component: SettingsComponent },
{ path: '**', redirectTo: '/dashboard' } { path: '**', redirectTo: '/dashboard' },
]; ];

View file

@ -6,7 +6,7 @@ describe('App', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [App], imports: [App],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}).compileComponents(); }).compileComponents();
}); });

View file

@ -1,13 +1,13 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button'; import { RouterOutlet } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
import { MatChipsModule } from '@angular/material/chips';
import { SidebarComponent } from './components/sidebar/sidebar.component';
import { NotificationsComponent } from './components/notifications/notifications'; import { NotificationsComponent } from './components/notifications/notifications';
import { SidebarComponent } from './components/sidebar/sidebar.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -20,10 +20,10 @@ import { NotificationsComponent } from './components/notifications/notifications
MatIconModule, MatIconModule,
MatChipsModule, MatChipsModule,
SidebarComponent, SidebarComponent,
NotificationsComponent NotificationsComponent,
], ],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.css' styleUrl: './app.css',
}) })
export class App { export class App {
protected title = 'Trading Dashboard'; protected title = 'Trading Dashboard';

View file

@ -1,12 +1,12 @@
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon'; import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
import { MatMenuModule } from '@angular/material/menu'; import { MatButtonModule } from '@angular/material/button';
import { MatListModule } from '@angular/material/list';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
import { NotificationService, Notification } from '../../services/notification.service'; import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { Notification, NotificationService } from '../../services/notification.service';
@Component({ @Component({
selector: 'app-notifications', selector: 'app-notifications',
@ -17,10 +17,10 @@ import { NotificationService, Notification } from '../../services/notification.s
MatBadgeModule, MatBadgeModule,
MatMenuModule, MatMenuModule,
MatListModule, MatListModule,
MatDividerModule MatDividerModule,
], ],
templateUrl: './notifications.html', templateUrl: './notifications.html',
styleUrl: './notifications.css' styleUrl: './notifications.css',
}) })
export class NotificationsComponent { export class NotificationsComponent {
private notificationService = inject(NotificationService); private notificationService = inject(NotificationService);
@ -51,21 +51,29 @@ export class NotificationsComponent {
getNotificationIcon(type: string): string { getNotificationIcon(type: string): string {
switch (type) { switch (type) {
case 'error': return 'error'; case 'error':
case 'warning': return 'warning'; return 'error';
case 'success': return 'check_circle'; case 'warning':
return 'warning';
case 'success':
return 'check_circle';
case 'info': case 'info':
default: return 'info'; default:
return 'info';
} }
} }
getNotificationColor(type: string): string { getNotificationColor(type: string): string {
switch (type) { switch (type) {
case 'error': return 'text-red-600'; case 'error':
case 'warning': return 'text-yellow-600'; return 'text-red-600';
case 'success': return 'text-green-600'; case 'warning':
return 'text-yellow-600';
case 'success':
return 'text-green-600';
case 'info': case 'info':
default: return 'text-blue-600'; default:
return 'text-blue-600';
} }
} }

View file

@ -1,9 +1,9 @@
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatSidenavModule } from '@angular/material/sidenav'; import { Component, input, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { Router, NavigationEnd } from '@angular/router'; import { MatSidenavModule } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
export interface NavigationItem { export interface NavigationItem {
@ -16,14 +16,9 @@ export interface NavigationItem {
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
standalone: true, standalone: true,
imports: [ imports: [CommonModule, MatSidenavModule, MatButtonModule, MatIconModule],
CommonModule,
MatSidenavModule,
MatButtonModule,
MatIconModule
],
templateUrl: './sidebar.component.html', templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css' styleUrl: './sidebar.component.css',
}) })
export class SidebarComponent { export class SidebarComponent {
opened = input<boolean>(true); opened = input<boolean>(true);
@ -35,14 +30,14 @@ export class SidebarComponent {
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' }, { label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
{ label: 'Strategies', icon: 'psychology', route: '/strategies' }, { label: 'Strategies', icon: 'psychology', route: '/strategies' },
{ label: 'Risk Management', icon: 'security', route: '/risk-management' }, { label: 'Risk Management', icon: 'security', route: '/risk-management' },
{ label: 'Settings', icon: 'settings', route: '/settings' } { label: 'Settings', icon: 'settings', route: '/settings' },
]; ];
constructor(private router: Router) { constructor(private router: Router) {
// Listen to route changes to update active state // Listen to route changes to update active state
this.router.events.pipe( this.router.events
filter(event => event instanceof NavigationEnd) .pipe(filter(event => event instanceof NavigationEnd))
).subscribe((event: NavigationEnd) => { .subscribe((event: NavigationEnd) => {
this.updateActiveRoute(event.urlAfterRedirects); this.updateActiveRoute(event.urlAfterRedirects);
}); });
} }

View file

@ -1,10 +1,10 @@
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { Component, signal } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
export interface MarketDataItem { export interface MarketDataItem {
symbol: string; symbol: string;
@ -22,23 +22,23 @@ export interface MarketDataItem {
MatTabsModule, MatTabsModule,
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
MatTableModule MatTableModule,
], ],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css' styleUrl: './dashboard.component.css',
}) })
export class DashboardComponent { export class DashboardComponent {
// Mock data for the dashboard // Mock data for the dashboard
protected marketData = signal<MarketDataItem[]>([ protected marketData = signal<MarketDataItem[]>([
{ symbol: 'AAPL', price: 192.53, change: 2.41, changePercent: 1.27 }, { symbol: 'AAPL', price: 192.53, change: 2.41, changePercent: 1.27 },
{ symbol: 'GOOGL', price: 138.21, change: -1.82, changePercent: -1.30 }, { symbol: 'GOOGL', price: 138.21, change: -1.82, changePercent: -1.3 },
{ symbol: 'MSFT', price: 378.85, change: 4.12, changePercent: 1.10 }, { symbol: 'MSFT', price: 378.85, change: 4.12, changePercent: 1.1 },
{ symbol: 'TSLA', price: 248.42, change: -3.21, changePercent: -1.28 }, { symbol: 'TSLA', price: 248.42, change: -3.21, changePercent: -1.28 },
]); ]);
protected portfolioValue = signal(125420.50); protected portfolioValue = signal(125420.5);
protected dayChange = signal(2341.20); protected dayChange = signal(2341.2);
protected dayChangePercent = signal(1.90); protected dayChangePercent = signal(1.9);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent']; protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent'];
} }

View file

@ -1,15 +1,15 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { interval, Subscription } from 'rxjs';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { WebSocketService } from '../../services/websocket.service'; import { WebSocketService } from '../../services/websocket.service';
import { interval, Subscription } from 'rxjs';
export interface ExtendedMarketData { export interface ExtendedMarketData {
symbol: string; symbol: string;
@ -33,10 +33,10 @@ export interface ExtendedMarketData {
MatTableModule, MatTableModule,
MatTabsModule, MatTabsModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatSnackBarModule MatSnackBarModule,
], ],
templateUrl: './market-data.component.html', templateUrl: './market-data.component.html',
styleUrl: './market-data.component.css' styleUrl: './market-data.component.css',
}) })
export class MarketDataComponent implements OnInit, OnDestroy { export class MarketDataComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService); private apiService = inject(ApiService);
@ -48,7 +48,14 @@ export class MarketDataComponent implements OnInit, OnDestroy {
protected currentTime = signal<string>(new Date().toLocaleTimeString()); protected currentTime = signal<string>(new Date().toLocaleTimeString());
protected isLoading = signal<boolean>(true); protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null); protected error = signal<string | null>(null);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent', 'volume', 'marketCap']; protected displayedColumns: string[] = [
'symbol',
'price',
'change',
'changePercent',
'volume',
'marketCap',
];
ngOnInit() { ngOnInit() {
// Update time every second // Update time every second
const timeSubscription = interval(1000).subscribe(() => { const timeSubscription = interval(1000).subscribe(() => {
@ -61,12 +68,12 @@ export class MarketDataComponent implements OnInit, OnDestroy {
// Subscribe to real-time market data updates // Subscribe to real-time market data updates
const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({ const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({
next: (update) => { next: update => {
this.updateMarketData(update); this.updateMarketData(update);
}, },
error: (err) => { error: err => {
console.error('WebSocket market data error:', err); console.error('WebSocket market data error:', err);
} },
}); });
this.subscriptions.push(wsSubscription); this.subscriptions.push(wsSubscription);
@ -84,20 +91,20 @@ export class MarketDataComponent implements OnInit, OnDestroy {
} }
private loadMarketData() { private loadMarketData() {
this.apiService.getMarketData().subscribe({ this.apiService.getMarketData().subscribe({
next: (response) => { next: response => {
// Convert MarketData to ExtendedMarketData with mock extended properties // Convert MarketData to ExtendedMarketData with mock extended properties
const extendedData: ExtendedMarketData[] = response.data.map(item => ({ const extendedData: ExtendedMarketData[] = response.data.map(item => ({
...item, ...item,
marketCap: this.getMockMarketCap(item.symbol), marketCap: this.getMockMarketCap(item.symbol),
high52Week: item.price * 1.3, // Mock 52-week high (30% above current) high52Week: item.price * 1.3, // Mock 52-week high (30% above current)
low52Week: item.price * 0.7 // Mock 52-week low (30% below current) low52Week: item.price * 0.7, // Mock 52-week low (30% below current)
})); }));
this.marketData.set(extendedData); this.marketData.set(extendedData);
this.isLoading.set(false); this.isLoading.set(false);
this.error.set(null); this.error.set(null);
}, },
error: (err) => { error: err => {
console.error('Failed to load market data:', err); console.error('Failed to load market data:', err);
this.error.set('Failed to load market data'); this.error.set('Failed to load market data');
this.isLoading.set(false); this.isLoading.set(false);
@ -105,17 +112,17 @@ export class MarketDataComponent implements OnInit, OnDestroy {
// Use mock data as fallback // Use mock data as fallback
this.marketData.set(this.getMockData()); this.marketData.set(this.getMockData());
} },
}); });
} }
private getMockMarketCap(symbol: string): string { private getMockMarketCap(symbol: string): string {
const marketCaps: { [key: string]: string } = { const marketCaps: { [key: string]: string } = {
'AAPL': '2.98T', AAPL: '2.98T',
'GOOGL': '1.78T', GOOGL: '1.78T',
'MSFT': '3.08T', MSFT: '3.08T',
'TSLA': '789.2B', TSLA: '789.2B',
'AMZN': '1.59T' AMZN: '1.59T',
}; };
return marketCaps[symbol] || '1.00T'; return marketCaps[symbol] || '1.00T';
} }
@ -130,7 +137,7 @@ export class MarketDataComponent implements OnInit, OnDestroy {
volume: 45230000, volume: 45230000,
marketCap: '2.98T', marketCap: '2.98T',
high52Week: 199.62, high52Week: 199.62,
low52Week: 164.08 low52Week: 164.08,
}, },
{ {
symbol: 'GOOGL', symbol: 'GOOGL',
@ -140,7 +147,7 @@ export class MarketDataComponent implements OnInit, OnDestroy {
volume: 12450000, volume: 12450000,
marketCap: '1.78T', marketCap: '1.78T',
high52Week: 3030.93, high52Week: 3030.93,
low52Week: 2193.62 low52Week: 2193.62,
}, },
{ {
symbol: 'MSFT', symbol: 'MSFT',
@ -150,17 +157,17 @@ export class MarketDataComponent implements OnInit, OnDestroy {
volume: 23180000, volume: 23180000,
marketCap: '3.08T', marketCap: '3.08T',
high52Week: 468.35, high52Week: 468.35,
low52Week: 309.45 low52Week: 309.45,
}, },
{ {
symbol: 'TSLA', symbol: 'TSLA',
price: 248.50, price: 248.5,
change: -5.21, change: -5.21,
changePercent: -2.05, changePercent: -2.05,
volume: 89760000, volume: 89760000,
marketCap: '789.2B', marketCap: '789.2B',
high52Week: 299.29, high52Week: 299.29,
low52Week: 152.37 low52Week: 152.37,
}, },
{ {
symbol: 'AMZN', symbol: 'AMZN',
@ -170,8 +177,8 @@ export class MarketDataComponent implements OnInit, OnDestroy {
volume: 34520000, volume: 34520000,
marketCap: '1.59T', marketCap: '1.59T',
high52Week: 170.17, high52Week: 170.17,
low52Week: 118.35 low52Week: 118.35,
} },
]; ];
} }
refreshData() { refreshData() {
@ -188,7 +195,7 @@ export class MarketDataComponent implements OnInit, OnDestroy {
price: update.price, price: update.price,
change: update.change, change: update.change,
changePercent: update.changePercent, changePercent: update.changePercent,
volume: update.volume volume: update.volume,
}; };
} }
return item; return item;

View file

@ -1,14 +1,14 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { ApiService } from '../../services/api.service';
import { interval, Subscription } from 'rxjs'; import { interval, Subscription } from 'rxjs';
import { ApiService } from '../../services/api.service';
export interface Position { export interface Position {
symbol: string; symbol: string;
@ -44,10 +44,10 @@ export interface PortfolioSummary {
MatTableModule, MatTableModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatSnackBarModule, MatSnackBarModule,
MatTabsModule MatTabsModule,
], ],
templateUrl: './portfolio.component.html', templateUrl: './portfolio.component.html',
styleUrl: './portfolio.component.css' styleUrl: './portfolio.component.css',
}) })
export class PortfolioComponent implements OnInit, OnDestroy { export class PortfolioComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService); private apiService = inject(ApiService);
@ -62,13 +62,21 @@ export class PortfolioComponent implements OnInit, OnDestroy {
dayChange: 0, dayChange: 0,
dayChangePercent: 0, dayChangePercent: 0,
cash: 0, cash: 0,
positionsCount: 0 positionsCount: 0,
}); });
protected positions = signal<Position[]>([]); protected positions = signal<Position[]>([]);
protected isLoading = signal<boolean>(true); protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null); protected error = signal<string | null>(null);
protected displayedColumns = ['symbol', 'quantity', 'avgPrice', 'currentPrice', 'marketValue', 'unrealizedPnL', 'dayChange']; protected displayedColumns = [
'symbol',
'quantity',
'avgPrice',
'currentPrice',
'marketValue',
'unrealizedPnL',
'dayChange',
];
ngOnInit() { ngOnInit() {
this.loadPortfolioData(); this.loadPortfolioData();
@ -93,51 +101,52 @@ export class PortfolioComponent implements OnInit, OnDestroy {
{ {
symbol: 'AAPL', symbol: 'AAPL',
quantity: 100, quantity: 100,
avgPrice: 180.50, avgPrice: 180.5,
currentPrice: 192.53, currentPrice: 192.53,
marketValue: 19253, marketValue: 19253,
unrealizedPnL: 1203, unrealizedPnL: 1203,
unrealizedPnLPercent: 6.67, unrealizedPnLPercent: 6.67,
dayChange: 241, dayChange: 241,
dayChangePercent: 1.27 dayChangePercent: 1.27,
}, },
{ {
symbol: 'MSFT', symbol: 'MSFT',
quantity: 50, quantity: 50,
avgPrice: 400.00, avgPrice: 400.0,
currentPrice: 415.26, currentPrice: 415.26,
marketValue: 20763, marketValue: 20763,
unrealizedPnL: 763, unrealizedPnL: 763,
unrealizedPnLPercent: 3.82, unrealizedPnLPercent: 3.82,
dayChange: 436.50, dayChange: 436.5,
dayChangePercent: 2.15 dayChangePercent: 2.15,
}, },
{ {
symbol: 'GOOGL', symbol: 'GOOGL',
quantity: 10, quantity: 10,
avgPrice: 2900.00, avgPrice: 2900.0,
currentPrice: 2847.56, currentPrice: 2847.56,
marketValue: 28475.60, marketValue: 28475.6,
unrealizedPnL: -524.40, unrealizedPnL: -524.4,
unrealizedPnLPercent: -1.81, unrealizedPnLPercent: -1.81,
dayChange: -123.40, dayChange: -123.4,
dayChangePercent: -0.43 dayChangePercent: -0.43,
} },
]; ];
const summary: PortfolioSummary = { const summary: PortfolioSummary = {
totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash
totalCost: mockPositions.reduce((sum, pos) => sum + (pos.avgPrice * pos.quantity), 0), totalCost: mockPositions.reduce((sum, pos) => sum + pos.avgPrice * pos.quantity, 0),
totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0), totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0),
totalPnLPercent: 0, totalPnLPercent: 0,
dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0), dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0),
dayChangePercent: 0, dayChangePercent: 0,
cash: 25000, cash: 25000,
positionsCount: mockPositions.length positionsCount: mockPositions.length,
}; };
summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100; summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100;
summary.dayChangePercent = (summary.dayChange / (summary.totalValue - summary.dayChange)) * 100; summary.dayChangePercent =
(summary.dayChange / (summary.totalValue - summary.dayChange)) * 100;
this.positions.set(mockPositions); this.positions.set(mockPositions);
this.portfolioSummary.set(summary); this.portfolioSummary.set(summary);

View file

@ -1,16 +1,16 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { MatIconModule } from '@angular/material/icon'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table'; import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { ApiService, RiskThresholds, RiskEvaluation } from '../../services/api.service'; import { MatTableModule } from '@angular/material/table';
import { interval, Subscription } from 'rxjs'; import { interval, Subscription } from 'rxjs';
import { ApiService, RiskEvaluation, RiskThresholds } from '../../services/api.service';
@Component({ @Component({
selector: 'app-risk-management', selector: 'app-risk-management',
@ -25,10 +25,10 @@ import { interval, Subscription } from 'rxjs';
MatInputModule, MatInputModule,
MatSnackBarModule, MatSnackBarModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
ReactiveFormsModule ReactiveFormsModule,
], ],
templateUrl: './risk-management.component.html', templateUrl: './risk-management.component.html',
styleUrl: './risk-management.component.css' styleUrl: './risk-management.component.css',
}) })
export class RiskManagementComponent implements OnInit, OnDestroy { export class RiskManagementComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService); private apiService = inject(ApiService);
@ -50,7 +50,7 @@ export class RiskManagementComponent implements OnInit, OnDestroy {
maxPositionSize: [0, [Validators.required, Validators.min(0)]], maxPositionSize: [0, [Validators.required, Validators.min(0)]],
maxDailyLoss: [0, [Validators.required, Validators.min(0)]], maxDailyLoss: [0, [Validators.required, Validators.min(0)]],
maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]], maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]] volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
}); });
} }
@ -71,30 +71,30 @@ export class RiskManagementComponent implements OnInit, OnDestroy {
private loadRiskThresholds() { private loadRiskThresholds() {
this.apiService.getRiskThresholds().subscribe({ this.apiService.getRiskThresholds().subscribe({
next: (response) => { next: response => {
this.riskThresholds.set(response.data); this.riskThresholds.set(response.data);
this.thresholdsForm.patchValue(response.data); this.thresholdsForm.patchValue(response.data);
this.isLoading.set(false); this.isLoading.set(false);
this.error.set(null); this.error.set(null);
}, },
error: (err) => { error: err => {
console.error('Failed to load risk thresholds:', err); console.error('Failed to load risk thresholds:', err);
this.error.set('Failed to load risk thresholds'); this.error.set('Failed to load risk thresholds');
this.isLoading.set(false); this.isLoading.set(false);
this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 }); this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 });
} },
}); });
} }
private loadRiskHistory() { private loadRiskHistory() {
this.apiService.getRiskHistory().subscribe({ this.apiService.getRiskHistory().subscribe({
next: (response) => { next: response => {
this.riskHistory.set(response.data); this.riskHistory.set(response.data);
}, },
error: (err) => { error: err => {
console.error('Failed to load risk history:', err); console.error('Failed to load risk history:', err);
this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 }); this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 });
} },
}); });
} }
@ -104,16 +104,16 @@ export class RiskManagementComponent implements OnInit, OnDestroy {
const thresholds = this.thresholdsForm.value as RiskThresholds; const thresholds = this.thresholdsForm.value as RiskThresholds;
this.apiService.updateRiskThresholds(thresholds).subscribe({ this.apiService.updateRiskThresholds(thresholds).subscribe({
next: (response) => { next: response => {
this.riskThresholds.set(response.data); this.riskThresholds.set(response.data);
this.isSaving.set(false); this.isSaving.set(false);
this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 }); this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 });
}, },
error: (err) => { error: err => {
console.error('Failed to save risk thresholds:', err); console.error('Failed to save risk thresholds:', err);
this.isSaving.set(false); this.isSaving.set(false);
this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 }); this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 });
} },
}); });
} }
} }
@ -126,10 +126,14 @@ export class RiskManagementComponent implements OnInit, OnDestroy {
getRiskLevelColor(level: string): string { getRiskLevelColor(level: string): string {
switch (level) { switch (level) {
case 'LOW': return 'text-green-600'; case 'LOW':
case 'MEDIUM': return 'text-yellow-600'; return 'text-green-600';
case 'HIGH': return 'text-red-600'; case 'MEDIUM':
default: return 'text-gray-600'; return 'text-yellow-600';
case 'HIGH':
return 'text-red-600';
default:
return 'text-gray-600';
} }
} }
} }

View file

@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@ -8,6 +8,6 @@ import { MatIconModule } from '@angular/material/icon';
standalone: true, standalone: true,
imports: [CommonModule, MatCardModule, MatIconModule], imports: [CommonModule, MatCardModule, MatIconModule],
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrl: './settings.component.css' styleUrl: './settings.component.css',
}) })
export class SettingsComponent {} export class SettingsComponent {}

View file

@ -1,7 +1,7 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Chart, ChartOptions } from 'chart.js/auto'; import { Chart, ChartOptions } from 'chart.js/auto';
import { BacktestResult } from '../../../services/strategy.service';
@Component({ @Component({
selector: 'app-drawdown-chart', selector: 'app-drawdown-chart',
@ -18,7 +18,7 @@ import { Chart, ChartOptions } from 'chart.js/auto';
height: 300px; height: 300px;
margin-bottom: 20px; margin-bottom: 20px;
} }
` `,
}) })
export class DrawdownChartComponent implements OnChanges { export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult; @Input() backtestResult?: BacktestResult;
@ -63,9 +63,9 @@ export class DrawdownChartComponent implements OnChanges {
backgroundColor: 'rgba(255, 99, 132, 0.2)', backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true, fill: true,
tension: 0.3, tension: 0.3,
borderWidth: 2 borderWidth: 2,
} },
] ],
}, },
options: { options: {
responsive: true, responsive: true,
@ -75,24 +75,24 @@ export class DrawdownChartComponent implements OnChanges {
ticks: { ticks: {
maxTicksLimit: 12, maxTicksLimit: 12,
maxRotation: 0, maxRotation: 0,
minRotation: 0 minRotation: 0,
}, },
grid: { grid: {
display: false display: false,
} },
}, },
y: { y: {
ticks: { ticks: {
callback: function (value) { callback: function (value) {
return (value * 100).toFixed(1) + '%'; return (value * 100).toFixed(1) + '%';
} },
}, },
grid: { grid: {
color: 'rgba(200, 200, 200, 0.2)' color: 'rgba(200, 200, 200, 0.2)',
}, },
min: -0.05, // Show at least 5% drawdown for context min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01 suggestedMax: 0.01,
} },
}, },
plugins: { plugins: {
tooltip: { tooltip: {
@ -108,14 +108,14 @@ export class DrawdownChartComponent implements OnChanges {
label += (context.parsed.y * 100).toFixed(2) + '%'; label += (context.parsed.y * 100).toFixed(2) + '%';
} }
return label; return label;
} },
} },
}, },
legend: { legend: {
position: 'top', position: 'top',
} },
} },
} as ChartOptions } as ChartOptions,
}); });
} }
@ -136,7 +136,7 @@ export class DrawdownChartComponent implements OnChanges {
const equityCurve: number[] = []; const equityCurve: number[] = [];
for (const daily of sortedReturns) { for (const daily of sortedReturns) {
equity *= (1 + daily.return); equity *= 1 + daily.return;
equityCurve.push(equity); equityCurve.push(equity);
dates.push(new Date(daily.date)); dates.push(new Date(daily.date));
} }
@ -148,7 +148,7 @@ export class DrawdownChartComponent implements OnChanges {
// Update high water mark // Update high water mark
hwm = Math.max(hwm, equityCurve[i]); hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark // Calculate drawdown as percentage from high water mark
const drawdown = (equityCurve[i] / hwm) - 1; const drawdown = equityCurve[i] / hwm - 1;
drawdowns.push(drawdown); drawdowns.push(drawdown);
} }
@ -159,7 +159,7 @@ export class DrawdownChartComponent implements OnChanges {
return new Date(date).toLocaleDateString('en-US', { return new Date(date).toLocaleDateString('en-US', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric' year: 'numeric',
}); });
} }
} }

View file

@ -1,7 +1,7 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Chart, ChartOptions } from 'chart.js/auto'; import { Chart, ChartOptions } from 'chart.js/auto';
import { BacktestResult } from '../../../services/strategy.service';
@Component({ @Component({
selector: 'app-equity-chart', selector: 'app-equity-chart',
@ -18,7 +18,7 @@ import { Chart, ChartOptions } from 'chart.js/auto';
height: 400px; height: 400px;
margin-bottom: 20px; margin-bottom: 20px;
} }
` `,
}) })
export class EquityChartComponent implements OnChanges { export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult; @Input() backtestResult?: BacktestResult;
@ -63,7 +63,7 @@ export class EquityChartComponent implements OnChanges {
backgroundColor: 'rgba(75, 192, 192, 0.2)', backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
fill: true fill: true,
}, },
{ {
label: 'Benchmark', label: 'Benchmark',
@ -73,9 +73,9 @@ export class EquityChartComponent implements OnChanges {
borderDash: [5, 5], borderDash: [5, 5],
tension: 0.3, tension: 0.3,
borderWidth: 1, borderWidth: 1,
fill: false fill: false,
} },
] ],
}, },
options: { options: {
responsive: true, responsive: true,
@ -85,22 +85,22 @@ export class EquityChartComponent implements OnChanges {
ticks: { ticks: {
maxTicksLimit: 12, maxTicksLimit: 12,
maxRotation: 0, maxRotation: 0,
minRotation: 0 minRotation: 0,
}, },
grid: { grid: {
display: false display: false,
} },
}, },
y: { y: {
ticks: { ticks: {
callback: function (value) { callback: function (value) {
return '$' + value.toLocaleString(); return '$' + value.toLocaleString();
} },
}, },
grid: { grid: {
color: 'rgba(200, 200, 200, 0.2)' color: 'rgba(200, 200, 200, 0.2)',
} },
} },
}, },
plugins: { plugins: {
tooltip: { tooltip: {
@ -113,18 +113,20 @@ export class EquityChartComponent implements OnChanges {
label += ': '; label += ': ';
} }
if (context.parsed.y !== null) { if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) label += new Intl.NumberFormat('en-US', {
.format(context.parsed.y); style: 'currency',
currency: 'USD',
}).format(context.parsed.y);
} }
return label; return label;
} },
} },
}, },
legend: { legend: {
position: 'top', position: 'top',
} },
} },
} as ChartOptions } as ChartOptions,
}); });
} }
@ -165,7 +167,7 @@ export class EquityChartComponent implements OnChanges {
return new Date(date).toLocaleDateString('en-US', { return new Date(date).toLocaleDateString('en-US', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric' year: 'numeric',
}); });
} }
} }

View file

@ -1,21 +1,15 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service'; import { BacktestResult } from '../../../services/strategy.service';
@Component({ @Component({
selector: 'app-performance-metrics', selector: 'app-performance-metrics',
standalone: true, standalone: true,
imports: [ imports: [CommonModule, MatCardModule, MatGridListModule, MatDividerModule, MatTooltipModule],
CommonModule,
MatCardModule,
MatGridListModule,
MatDividerModule,
MatTooltipModule
],
template: ` template: `
<mat-card class="metrics-card"> <mat-card class="metrics-card">
<mat-card-header> <mat-card-header>
@ -27,14 +21,27 @@ import { BacktestResult } from '../../../services/strategy.service';
<h3>Returns</h3> <h3>Returns</h3>
<div class="metrics-row"> <div class="metrics-row">
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">Total Return</div> <div class="metric-name" matTooltip="Total return over the backtest period">
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.totalReturn || 0)"> Total Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.totalReturn || 0)"
>
{{ formatPercent(backtestResult?.totalReturn || 0) }} {{ formatPercent(backtestResult?.totalReturn || 0) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Annualized return (adjusted for the backtest duration)">Annualized Return</div> <div
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)"> class="metric-name"
matTooltip="Annualized return (adjusted for the backtest duration)"
>
Annualized Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)"
>
{{ formatPercent(backtestResult?.annualizedReturn || 0) }} {{ formatPercent(backtestResult?.annualizedReturn || 0) }}
</div> </div>
</div> </div>
@ -53,25 +60,36 @@ import { BacktestResult } from '../../../services/strategy.service';
<h3>Risk Metrics</h3> <h3>Risk Metrics</h3>
<div class="metrics-row"> <div class="metrics-row">
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">Max Drawdown</div> <div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">
Max Drawdown
</div>
<div class="metric-value negative"> <div class="metric-value negative">
{{ formatPercent(backtestResult?.maxDrawdown || 0) }} {{ formatPercent(backtestResult?.maxDrawdown || 0) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">Max DD Duration</div> <div class="metric-name" matTooltip="Number of days in the worst drawdown">
Max DD Duration
</div>
<div class="metric-value"> <div class="metric-value">
{{ formatDays(backtestResult?.maxDrawdownDuration || 0) }} {{ formatDays(backtestResult?.maxDrawdownDuration || 0) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">Volatility</div> <div class="metric-name" matTooltip="Annualized standard deviation of returns">
Volatility
</div>
<div class="metric-value"> <div class="metric-value">
{{ formatPercent(backtestResult?.volatility || 0) }} {{ formatPercent(backtestResult?.volatility || 0) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Square root of the sum of the squares of drawdowns">Ulcer Index</div> <div
class="metric-name"
matTooltip="Square root of the sum of the squares of drawdowns"
>
Ulcer Index
</div>
<div class="metric-value"> <div class="metric-value">
{{ (backtestResult?.ulcerIndex || 0).toFixed(4) }} {{ (backtestResult?.ulcerIndex || 0).toFixed(4) }}
</div> </div>
@ -85,26 +103,49 @@ import { BacktestResult } from '../../../services/strategy.service';
<h3>Risk-Adjusted Returns</h3> <h3>Risk-Adjusted Returns</h3>
<div class="metrics-row"> <div class="metrics-row">
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">Sharpe Ratio</div> <div class="metric-name" matTooltip="Excess return per unit of risk">
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)"> Sharpe Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)"
>
{{ (backtestResult?.sharpeRatio || 0).toFixed(2) }} {{ (backtestResult?.sharpeRatio || 0).toFixed(2) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">Sortino Ratio</div> <div class="metric-name" matTooltip="Return per unit of downside risk">
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)"> Sortino Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)"
>
{{ (backtestResult?.sortinoRatio || 0).toFixed(2) }} {{ (backtestResult?.sortinoRatio || 0).toFixed(2) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">Calmar Ratio</div> <div class="metric-name" matTooltip="Return per unit of max drawdown">
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)"> Calmar Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)"
>
{{ (backtestResult?.calmarRatio || 0).toFixed(2) }} {{ (backtestResult?.calmarRatio || 0).toFixed(2) }}
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Probability-weighted ratio of gains vs. losses">Omega Ratio</div> <div
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)"> class="metric-name"
matTooltip="Probability-weighted ratio of gains vs. losses"
>
Omega Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)"
>
{{ (backtestResult?.omegaRatio || 0).toFixed(2) }} {{ (backtestResult?.omegaRatio || 0).toFixed(2) }}
</div> </div>
</div> </div>
@ -141,8 +182,13 @@ import { BacktestResult } from '../../../services/strategy.service';
</div> </div>
</div> </div>
<div class="metric"> <div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">Profit Factor</div> <div class="metric-name" matTooltip="Ratio of total gains to total losses">
<div class="metric-value" [ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)"> Profit Factor
</div>
<div
class="metric-value"
[ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)"
>
{{ (backtestResult?.profitFactor || 0).toFixed(2) }} {{ (backtestResult?.profitFactor || 0).toFixed(2) }}
</div> </div>
</div> </div>
@ -198,21 +244,21 @@ import { BacktestResult } from '../../../services/strategy.service';
} }
.positive { .positive {
color: #4CAF50; color: #4caf50;
} }
.negative { .negative {
color: #F44336; color: #f44336;
} }
.neutral { .neutral {
color: #FFA000; color: #ffa000;
} }
mat-divider { mat-divider {
margin: 8px 0; margin: 8px 0;
} }
` `,
}) })
export class PerformanceMetricsComponent { export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult; @Input() backtestResult?: BacktestResult;
@ -222,7 +268,7 @@ export class PerformanceMetricsComponent {
return new Intl.NumberFormat('en-US', { return new Intl.NumberFormat('en-US', {
style: 'percent', style: 'percent',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2 maximumFractionDigits: 2,
}).format(value); }).format(value);
} }

View file

@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table'; import { Component, Input } from '@angular/core';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { BacktestResult } from '../../../services/strategy.service'; import { BacktestResult } from '../../../services/strategy.service';
@Component({ @Component({
@ -16,7 +16,7 @@ import { BacktestResult } from '../../../services/strategy.service';
MatSortModule, MatSortModule,
MatPaginatorModule, MatPaginatorModule,
MatCardModule, MatCardModule,
MatIconModule MatIconModule,
], ],
template: ` template: `
<mat-card class="trades-card"> <mat-card class="trades-card">
@ -24,8 +24,13 @@ import { BacktestResult } from '../../../services/strategy.service';
<mat-card-title>Trades</mat-card-title> <mat-card-title>Trades</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<table mat-table [dataSource]="displayedTrades" matSort (matSortChange)="sortData($event)" class="trades-table"> <table
mat-table
[dataSource]="displayedTrades"
matSort
(matSortChange)="sortData($event)"
class="trades-table"
>
<!-- Symbol Column --> <!-- Symbol Column -->
<ng-container matColumnDef="symbol"> <ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Symbol</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Symbol</th>
@ -65,8 +70,11 @@ import { BacktestResult } from '../../../services/strategy.service';
<!-- P&L Column --> <!-- P&L Column -->
<ng-container matColumnDef="pnl"> <ng-container matColumnDef="pnl">
<th mat-header-cell *matHeaderCellDef mat-sort-header>P&L</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>P&L</th>
<td mat-cell *matCellDef="let trade" <td
[ngClass]="{'positive': trade.pnl > 0, 'negative': trade.pnl < 0}"> mat-cell
*matCellDef="let trade"
[ngClass]="{ positive: trade.pnl > 0, negative: trade.pnl < 0 }"
>
{{ formatCurrency(trade.pnl) }} {{ formatCurrency(trade.pnl) }}
</td> </td>
</ng-container> </ng-container>
@ -74,14 +82,17 @@ import { BacktestResult } from '../../../services/strategy.service';
<!-- P&L Percent Column --> <!-- P&L Percent Column -->
<ng-container matColumnDef="pnlPercent"> <ng-container matColumnDef="pnlPercent">
<th mat-header-cell *matHeaderCellDef mat-sort-header>P&L %</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>P&L %</th>
<td mat-cell *matCellDef="let trade" <td
[ngClass]="{'positive': trade.pnlPercent > 0, 'negative': trade.pnlPercent < 0}"> mat-cell
*matCellDef="let trade"
[ngClass]="{ positive: trade.pnlPercent > 0, negative: trade.pnlPercent < 0 }"
>
{{ formatPercent(trade.pnlPercent) }} {{ formatPercent(trade.pnlPercent) }}
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table> </table>
<mat-paginator <mat-paginator
@ -89,7 +100,8 @@ import { BacktestResult } from '../../../services/strategy.service';
[pageSize]="pageSize" [pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50]" [pageSizeOptions]="[5, 10, 25, 50]"
(page)="pageChange($event)" (page)="pageChange($event)"
aria-label="Select page"> aria-label="Select page"
>
</mat-paginator> </mat-paginator>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -104,23 +116,24 @@ import { BacktestResult } from '../../../services/strategy.service';
border-collapse: collapse; border-collapse: collapse;
} }
.mat-column-pnl, .mat-column-pnlPercent { .mat-column-pnl,
.mat-column-pnlPercent {
text-align: right; text-align: right;
font-weight: 500; font-weight: 500;
} }
.positive { .positive {
color: #4CAF50; color: #4caf50;
} }
.negative { .negative {
color: #F44336; color: #f44336;
} }
.mat-mdc-row:hover { .mat-mdc-row:hover {
background-color: rgba(0, 0, 0, 0.04); background-color: rgba(0, 0, 0, 0.04);
} }
` `,
}) })
export class TradesTableComponent { export class TradesTableComponent {
@Input() set backtestResult(value: BacktestResult | undefined) { @Input() set backtestResult(value: BacktestResult | undefined) {
@ -138,8 +151,14 @@ export class TradesTableComponent {
// Table configuration // Table configuration
displayedColumns: string[] = [ displayedColumns: string[] = [
'symbol', 'entryTime', 'entryPrice', 'exitTime', 'symbol',
'exitPrice', 'quantity', 'pnl', 'pnlPercent' 'entryTime',
'entryPrice',
'exitTime',
'exitPrice',
'quantity',
'pnl',
'pnlPercent',
]; ];
// Pagination // Pagination
@ -160,20 +179,39 @@ export class TradesTableComponent {
const data = this._backtestResult?.trades.slice() || []; const data = this._backtestResult?.trades.slice() || [];
this.displayedTrades = data.sort((a, b) => { this.displayedTrades = data
.sort((a, b) => {
const isAsc = sort.direction === 'asc'; const isAsc = sort.direction === 'asc';
switch (sort.active) { switch (sort.active) {
case 'symbol': return this.compare(a.symbol, b.symbol, isAsc); case 'symbol':
case 'entryTime': return this.compare(new Date(a.entryTime).getTime(), new Date(b.entryTime).getTime(), isAsc); return this.compare(a.symbol, b.symbol, isAsc);
case 'entryPrice': return this.compare(a.entryPrice, b.entryPrice, isAsc); case 'entryTime':
case 'exitTime': return this.compare(new Date(a.exitTime).getTime(), new Date(b.exitTime).getTime(), isAsc); return this.compare(
case 'exitPrice': return this.compare(a.exitPrice, b.exitPrice, isAsc); new Date(a.entryTime).getTime(),
case 'quantity': return this.compare(a.quantity, b.quantity, isAsc); new Date(b.entryTime).getTime(),
case 'pnl': return this.compare(a.pnl, b.pnl, isAsc); isAsc
case 'pnlPercent': return this.compare(a.pnlPercent, b.pnlPercent, isAsc); );
default: return 0; case 'entryPrice':
return this.compare(a.entryPrice, b.entryPrice, isAsc);
case 'exitTime':
return this.compare(
new Date(a.exitTime).getTime(),
new Date(b.exitTime).getTime(),
isAsc
);
case 'exitPrice':
return this.compare(a.exitPrice, b.exitPrice, isAsc);
case 'quantity':
return this.compare(a.quantity, b.quantity, isAsc);
case 'pnl':
return this.compare(a.pnl, b.pnl, isAsc);
case 'pnlPercent':
return this.compare(a.pnlPercent, b.pnlPercent, isAsc);
default:
return 0;
} }
}).slice(this.currentPage * this.pageSize, (this.currentPage + 1) * this.pageSize); })
.slice(this.currentPage * this.pageSize, (this.currentPage + 1) * this.pageSize);
} }
// Handle page changes // Handle page changes
@ -211,7 +249,7 @@ export class TradesTableComponent {
return new Intl.NumberFormat('en-US', { return new Intl.NumberFormat('en-US', {
style: 'percent', style: 'percent',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2 maximumFractionDigits: 2,
}).format(value); }).format(value);
} }

View file

@ -1,28 +1,23 @@
import { Component, Inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import { Component, Inject, OnInit } from '@angular/core';
FormBuilder, import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTabsModule } from '@angular/material/tabs';
import { import {
BacktestRequest, BacktestRequest,
BacktestResult, BacktestResult,
StrategyService, StrategyService,
TradingStrategy TradingStrategy,
} from '../../../services/strategy.service'; } from '../../../services/strategy.service';
@Component({ @Component({
@ -42,15 +37,25 @@ import {
MatTabsModule, MatTabsModule,
MatChipsModule, MatChipsModule,
MatIconModule, MatIconModule,
MatSlideToggleModule MatSlideToggleModule,
], ],
templateUrl: './backtest-dialog.component.html', templateUrl: './backtest-dialog.component.html',
styleUrl: './backtest-dialog.component.css' styleUrl: './backtest-dialog.component.css',
}) })
export class BacktestDialogComponent implements OnInit { export class BacktestDialogComponent implements OnInit {
backtestForm: FormGroup; backtestForm: FormGroup;
strategyTypes: string[] = []; strategyTypes: string[] = [];
availableSymbols: string[] = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'SPY', 'QQQ']; availableSymbols: string[] = [
'AAPL',
'MSFT',
'GOOGL',
'AMZN',
'TSLA',
'META',
'NVDA',
'SPY',
'QQQ',
];
selectedSymbols: string[] = []; selectedSymbols: string[] = [];
parameters: Record<string, any> = {}; parameters: Record<string, any> = {};
isRunning: boolean = false; isRunning: boolean = false;
@ -65,20 +70,23 @@ export class BacktestDialogComponent implements OnInit {
// Initialize form with defaults // Initialize form with defaults
this.backtestForm = this.fb.group({ this.backtestForm = this.fb.group({
strategyType: ['', [Validators.required]], strategyType: ['', [Validators.required]],
startDate: [new Date(new Date().setFullYear(new Date().getFullYear() - 1)), [Validators.required]], startDate: [
new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
[Validators.required],
],
endDate: [new Date(), [Validators.required]], endDate: [new Date(), [Validators.required]],
initialCapital: [100000, [Validators.required, Validators.min(1000)]], initialCapital: [100000, [Validators.required, Validators.min(1000)]],
dataResolution: ['1d', [Validators.required]], dataResolution: ['1d', [Validators.required]],
commission: [0.001, [Validators.required, Validators.min(0), Validators.max(0.1)]], commission: [0.001, [Validators.required, Validators.min(0), Validators.max(0.1)]],
slippage: [0.0005, [Validators.required, Validators.min(0), Validators.max(0.1)]], slippage: [0.0005, [Validators.required, Validators.min(0), Validators.max(0.1)]],
mode: ['event', [Validators.required]] mode: ['event', [Validators.required]],
}); });
// If strategy is provided, pre-populate the form // If strategy is provided, pre-populate the form
if (data) { if (data) {
this.selectedSymbols = [...data.symbols]; this.selectedSymbols = [...data.symbols];
this.backtestForm.patchValue({ this.backtestForm.patchValue({
strategyType: data.type strategyType: data.type,
}); });
this.parameters = { ...data.parameters }; this.parameters = { ...data.parameters };
} }
@ -90,7 +98,7 @@ export class BacktestDialogComponent implements OnInit {
loadStrategyTypes(): void { loadStrategyTypes(): void {
this.strategyService.getStrategyTypes().subscribe({ this.strategyService.getStrategyTypes().subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.strategyTypes = response.data; this.strategyTypes = response.data;
@ -100,33 +108,33 @@ export class BacktestDialogComponent implements OnInit {
} }
} }
}, },
error: (error) => { error: error => {
console.error('Error loading strategy types:', error); console.error('Error loading strategy types:', error);
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM']; this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
} },
}); });
} }
onStrategyTypeChange(type: string): void { onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type // Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({ this.strategyService.getStrategyParameters(type).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
// If strategy is provided, merge default with existing // If strategy is provided, merge default with existing
if (this.data) { if (this.data) {
this.parameters = { this.parameters = {
...response.data, ...response.data,
...this.data.parameters ...this.data.parameters,
}; };
} else { } else {
this.parameters = response.data; this.parameters = response.data;
} }
} }
}, },
error: (error) => { error: error => {
console.error('Error loading parameters:', error); console.error('Error loading parameters:', error);
this.parameters = {}; this.parameters = {};
} },
}); });
} }
@ -160,22 +168,22 @@ export class BacktestDialogComponent implements OnInit {
dataResolution: formValue.dataResolution, dataResolution: formValue.dataResolution,
commission: formValue.commission, commission: formValue.commission,
slippage: formValue.slippage, slippage: formValue.slippage,
mode: formValue.mode mode: formValue.mode,
}; };
this.isRunning = true; this.isRunning = true;
this.strategyService.runBacktest(backtestRequest).subscribe({ this.strategyService.runBacktest(backtestRequest).subscribe({
next: (response) => { next: response => {
this.isRunning = false; this.isRunning = false;
if (response.success) { if (response.success) {
this.backtestResult = response.data; this.backtestResult = response.data;
} }
}, },
error: (error) => { error: error => {
this.isRunning = false; this.isRunning = false;
console.error('Backtest error:', error); console.error('Backtest error:', error);
} },
}); });
} }

View file

@ -1,24 +1,16 @@
import { Component, Inject, OnInit } from '@angular/core'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import { Component, Inject, OnInit } from '@angular/core';
FormBuilder, import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
FormGroup, import { MatAutocompleteModule } from '@angular/material/autocomplete';
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatChipsModule } from '@angular/material/chips';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatChipsModule } from '@angular/material/chips'; import { StrategyService, TradingStrategy } from '../../../services/strategy.service';
import { MatIconModule } from '@angular/material/icon';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import {
StrategyService,
TradingStrategy
} from '../../../services/strategy.service';
@Component({ @Component({
selector: 'app-strategy-dialog', selector: 'app-strategy-dialog',
@ -33,16 +25,26 @@ import {
MatSelectModule, MatSelectModule,
MatChipsModule, MatChipsModule,
MatIconModule, MatIconModule,
MatAutocompleteModule MatAutocompleteModule,
], ],
templateUrl: './strategy-dialog.component.html', templateUrl: './strategy-dialog.component.html',
styleUrl: './strategy-dialog.component.css' styleUrl: './strategy-dialog.component.css',
}) })
export class StrategyDialogComponent implements OnInit { export class StrategyDialogComponent implements OnInit {
strategyForm: FormGroup; strategyForm: FormGroup;
isEditMode: boolean = false; isEditMode: boolean = false;
strategyTypes: string[] = []; strategyTypes: string[] = [];
availableSymbols: string[] = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'SPY', 'QQQ']; availableSymbols: string[] = [
'AAPL',
'MSFT',
'GOOGL',
'AMZN',
'TSLA',
'META',
'NVDA',
'SPY',
'QQQ',
];
selectedSymbols: string[] = []; selectedSymbols: string[] = [];
separatorKeysCodes: number[] = [ENTER, COMMA]; separatorKeysCodes: number[] = [ENTER, COMMA];
parameters: Record<string, any> = {}; parameters: Record<string, any> = {};
@ -67,7 +69,7 @@ export class StrategyDialogComponent implements OnInit {
this.strategyForm.patchValue({ this.strategyForm.patchValue({
name: data.name, name: data.name,
description: data.description, description: data.description,
type: data.type type: data.type,
}); });
this.parameters = { ...data.parameters }; this.parameters = { ...data.parameters };
} }
@ -81,7 +83,7 @@ export class StrategyDialogComponent implements OnInit {
loadStrategyTypes(): void { loadStrategyTypes(): void {
// In a real implementation, this would call the API // In a real implementation, this would call the API
this.strategyService.getStrategyTypes().subscribe({ this.strategyService.getStrategyTypes().subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.strategyTypes = response.data; this.strategyTypes = response.data;
@ -91,35 +93,35 @@ export class StrategyDialogComponent implements OnInit {
} }
} }
}, },
error: (error) => { error: error => {
console.error('Error loading strategy types:', error); console.error('Error loading strategy types:', error);
// Fallback to hardcoded types // Fallback to hardcoded types
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM']; this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
} },
}); });
} }
onStrategyTypeChange(type: string): void { onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type // Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({ this.strategyService.getStrategyParameters(type).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
// If editing, merge default with existing // If editing, merge default with existing
if (this.isEditMode && this.data) { if (this.isEditMode && this.data) {
this.parameters = { this.parameters = {
...response.data, ...response.data,
...this.data.parameters ...this.data.parameters,
}; };
} else { } else {
this.parameters = response.data; this.parameters = response.data;
} }
} }
}, },
error: (error) => { error: error => {
console.error('Error loading parameters:', error); console.error('Error loading parameters:', error);
// Fallback to empty parameters // Fallback to empty parameters
this.parameters = {}; this.parameters = {};
} },
}); });
} }
@ -149,25 +151,25 @@ export class StrategyDialogComponent implements OnInit {
if (this.isEditMode && this.data) { if (this.isEditMode && this.data) {
this.strategyService.updateStrategy(this.data.id, strategy).subscribe({ this.strategyService.updateStrategy(this.data.id, strategy).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.dialogRef.close(true); this.dialogRef.close(true);
} }
}, },
error: (error) => { error: error => {
console.error('Error updating strategy:', error); console.error('Error updating strategy:', error);
} },
}); });
} else { } else {
this.strategyService.createStrategy(strategy).subscribe({ this.strategyService.createStrategy(strategy).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.dialogRef.close(true); this.dialogRef.close(true);
} }
}, },
error: (error) => { error: error => {
console.error('Error creating strategy:', error); console.error('Error creating strategy:', error);
} },
}); });
} }
} }

View file

@ -1,21 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { Component, OnInit } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { MatChipsModule } from '@angular/material/chips';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { StrategyService, TradingStrategy } from '../../services/strategy.service'; import { StrategyService, TradingStrategy } from '../../services/strategy.service';
import { WebSocketService } from '../../services/websocket.service'; import { WebSocketService } from '../../services/websocket.service';
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
import { BacktestDialogComponent } from './dialogs/backtest-dialog.component'; import { BacktestDialogComponent } from './dialogs/backtest-dialog.component';
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
import { StrategyDetailsComponent } from './strategy-details/strategy-details.component'; import { StrategyDetailsComponent } from './strategy-details/strategy-details.component';
@Component({ @Component({
@ -36,10 +36,10 @@ import { StrategyDetailsComponent } from './strategy-details/strategy-details.co
MatProgressBarModule, MatProgressBarModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
StrategyDetailsComponent StrategyDetailsComponent,
], ],
templateUrl: './strategies.component.html', templateUrl: './strategies.component.html',
styleUrl: './strategies.component.css' styleUrl: './strategies.component.css',
}) })
export class StrategiesComponent implements OnInit { export class StrategiesComponent implements OnInit {
strategies: TradingStrategy[] = []; strategies: TradingStrategy[] = [];
@ -61,24 +61,26 @@ export class StrategiesComponent implements OnInit {
loadStrategies(): void { loadStrategies(): void {
this.isLoading = true; this.isLoading = true;
this.strategyService.getStrategies().subscribe({ this.strategyService.getStrategies().subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.strategies = response.data; this.strategies = response.data;
} }
this.isLoading = false; this.isLoading = false;
}, },
error: (error) => { error: error => {
console.error('Error loading strategies:', error); console.error('Error loading strategies:', error);
this.isLoading = false; this.isLoading = false;
} },
}); });
} }
listenForStrategyUpdates(): void { listenForStrategyUpdates(): void {
this.webSocketService.messages.subscribe(message => { this.webSocketService.messages.subscribe(message => {
if (message.type === 'STRATEGY_CREATED' || if (
message.type === 'STRATEGY_CREATED' ||
message.type === 'STRATEGY_UPDATED' || message.type === 'STRATEGY_UPDATED' ||
message.type === 'STRATEGY_STATUS_CHANGED') { message.type === 'STRATEGY_STATUS_CHANGED'
) {
// Refresh the strategy list when changes occur // Refresh the strategy list when changes occur
this.loadStrategies(); this.loadStrategies();
} }
@ -87,17 +89,21 @@ export class StrategiesComponent implements OnInit {
getStatusColor(status: string): string { getStatusColor(status: string): string {
switch (status) { switch (status) {
case 'ACTIVE': return 'green'; case 'ACTIVE':
case 'PAUSED': return 'orange'; return 'green';
case 'ERROR': return 'red'; case 'PAUSED':
default: return 'gray'; return 'orange';
case 'ERROR':
return 'red';
default:
return 'gray';
} }
} }
openStrategyDialog(strategy?: TradingStrategy): void { openStrategyDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(StrategyDialogComponent, { const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px', width: '600px',
data: strategy || null data: strategy || null,
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -110,7 +116,7 @@ export class StrategiesComponent implements OnInit {
openBacktestDialog(strategy?: TradingStrategy): void { openBacktestDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(BacktestDialogComponent, { const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px', width: '800px',
data: strategy || null data: strategy || null,
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -126,18 +132,18 @@ export class StrategiesComponent implements OnInit {
if (strategy.status === 'ACTIVE') { if (strategy.status === 'ACTIVE') {
this.strategyService.pauseStrategy(strategy.id).subscribe({ this.strategyService.pauseStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(), next: () => this.loadStrategies(),
error: (error) => { error: error => {
console.error('Error pausing strategy:', error); console.error('Error pausing strategy:', error);
this.isLoading = false; this.isLoading = false;
} },
}); });
} else { } else {
this.strategyService.startStrategy(strategy.id).subscribe({ this.strategyService.startStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(), next: () => this.loadStrategies(),
error: (error) => { error: error => {
console.error('Error starting strategy:', error); console.error('Error starting strategy:', error);
this.isLoading = false; this.isLoading = false;
} },
}); });
} }
} }

View file

@ -11,9 +11,11 @@
<button mat-raised-button color="primary" class="mr-2" (click)="openBacktestDialog()"> <button mat-raised-button color="primary" class="mr-2" (click)="openBacktestDialog()">
Run Backtest Run Backtest
</button> </button>
<span class="px-3 py-1 rounded-full text-xs font-semibold" <span
class="px-3 py-1 rounded-full text-xs font-semibold"
[style.background-color]="getStatusColor(strategy.status)" [style.background-color]="getStatusColor(strategy.status)"
style="color: white;"> style="color: white"
>
{{ strategy.status }} {{ strategy.status }}
</span> </span>
</div> </div>
@ -47,8 +49,13 @@
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <div>
<p class="text-sm text-gray-600">Return</p> <p class="text-sm text-gray-600">Return</p>
<p class="text-xl font-semibold" <p
[ngClass]="{'text-green-600': performance.totalReturn >= 0, 'text-red-600': performance.totalReturn < 0}"> class="text-xl font-semibold"
[ngClass]="{
'text-green-600': performance.totalReturn >= 0,
'text-red-600': performance.totalReturn < 0,
}"
>
{{ performance.totalReturn | percent: '1.2-2' }} {{ performance.totalReturn | percent: '1.2-2' }}
</p> </p>
</div> </div>
@ -62,7 +69,9 @@
</div> </div>
<div> <div>
<p class="text-sm text-gray-600">Max Drawdown</p> <p class="text-sm text-gray-600">Max Drawdown</p>
<p class="text-xl font-semibold text-red-600">{{performance.maxDrawdown | percent:'1.2-2'}}</p> <p class="text-xl font-semibold text-red-600">
{{ performance.maxDrawdown | percent: '1.2-2' }}
</p>
</div> </div>
<div> <div>
<p class="text-sm text-gray-600">Total Trades</p> <p class="text-sm text-gray-600">Total Trades</p>
@ -77,18 +86,31 @@
<mat-divider class="my-4"></mat-divider> <mat-divider class="my-4"></mat-divider>
<div class="flex justify-between mt-2"> <div class="flex justify-between mt-2">
<button mat-button color="primary" *ngIf="strategy.status !== 'ACTIVE'" (click)="activateStrategy()"> <button
mat-button
color="primary"
*ngIf="strategy.status !== 'ACTIVE'"
(click)="activateStrategy()"
>
<mat-icon>play_arrow</mat-icon> Start <mat-icon>play_arrow</mat-icon> Start
</button> </button>
<button mat-button color="accent" *ngIf="strategy.status === 'ACTIVE'" (click)="pauseStrategy()"> <button
mat-button
color="accent"
*ngIf="strategy.status === 'ACTIVE'"
(click)="pauseStrategy()"
>
<mat-icon>pause</mat-icon> Pause <mat-icon>pause</mat-icon> Pause
</button> </button>
<button mat-button color="warn" *ngIf="strategy.status === 'ACTIVE'" (click)="stopStrategy()"> <button
mat-button
color="warn"
*ngIf="strategy.status === 'ACTIVE'"
(click)="stopStrategy()"
>
<mat-icon>stop</mat-icon> Stop <mat-icon>stop</mat-icon> Stop
</button> </button>
<button mat-button (click)="openEditDialog()"> <button mat-button (click)="openEditDialog()"><mat-icon>edit</mat-icon> Edit</button>
<mat-icon>edit</mat-icon> Edit
</button>
</div> </div>
</mat-card> </mat-card>
</div> </div>
@ -143,9 +165,11 @@
<td class="py-2">{{ signal.timestamp | date: 'short' }}</td> <td class="py-2">{{ signal.timestamp | date: 'short' }}</td>
<td class="py-2">{{ signal.symbol }}</td> <td class="py-2">{{ signal.symbol }}</td>
<td class="py-2"> <td class="py-2">
<span class="px-2 py-1 rounded text-xs font-semibold" <span
class="px-2 py-1 rounded text-xs font-semibold"
[style.background-color]="getSignalColor(signal.action)" [style.background-color]="getSignalColor(signal.action)"
style="color: white;"> style="color: white"
>
{{ signal.action }} {{ signal.action }}
</span> </span>
</td> </td>
@ -181,16 +205,27 @@
<tr *ngFor="let trade of trades"> <tr *ngFor="let trade of trades">
<td class="py-2">{{ trade.symbol }}</td> <td class="py-2">{{ trade.symbol }}</td>
<td class="py-2"> <td class="py-2">
${{trade.entryPrice | number:'1.2-2'}} @ {{trade.entryTime | date:'short'}} ${{ trade.entryPrice | number: '1.2-2' }} &#64;
{{ trade.entryTime | date: 'short' }}
</td> </td>
<td class="py-2"> <td class="py-2">
${{trade.exitPrice | number:'1.2-2'}} @ {{trade.exitTime | date:'short'}} ${{ trade.exitPrice | number: '1.2-2' }} &#64;
{{ trade.exitTime | date: 'short' }}
</td> </td>
<td class="py-2">{{ trade.quantity }}</td> <td class="py-2">{{ trade.quantity }}</td>
<td class="py-2" [ngClass]="{'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0}"> <td
class="py-2"
[ngClass]="{ 'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0 }"
>
${{ trade.pnl | number: '1.2-2' }} ${{ trade.pnl | number: '1.2-2' }}
</td> </td>
<td class="py-2" [ngClass]="{'text-green-600': trade.pnlPercent >= 0, 'text-red-600': trade.pnlPercent < 0}"> <td
class="py-2"
[ngClass]="{
'text-green-600': trade.pnlPercent >= 0,
'text-red-600': trade.pnlPercent < 0,
}"
>
{{ trade.pnlPercent | number: '1.2-2' }}% {{ trade.pnlPercent | number: '1.2-2' }}%
</td> </td>
</tr> </tr>
@ -208,7 +243,7 @@
<mat-card class="p-6 flex items-center" *ngIf="!strategy"> <mat-card class="p-6 flex items-center" *ngIf="!strategy">
<div class="text-center text-gray-500 w-full"> <div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">psychology</mat-icon> <mat-icon style="font-size: 4rem; width: 4rem; height: 4rem">psychology</mat-icon>
<p class="mb-4">No strategy selected</p> <p class="mb-4">No strategy selected</p>
</div> </div>
</mat-card> </mat-card>

View file

@ -1,22 +1,26 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table'; import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { BacktestResult, TradingStrategy, StrategyService } from '../../../services/strategy.service'; import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import {
BacktestResult,
StrategyService,
TradingStrategy,
} from '../../../services/strategy.service';
import { WebSocketService } from '../../../services/websocket.service'; import { WebSocketService } from '../../../services/websocket.service';
import { EquityChartComponent } from '../components/equity-chart.component';
import { DrawdownChartComponent } from '../components/drawdown-chart.component'; import { DrawdownChartComponent } from '../components/drawdown-chart.component';
import { TradesTableComponent } from '../components/trades-table.component'; import { EquityChartComponent } from '../components/equity-chart.component';
import { PerformanceMetricsComponent } from '../components/performance-metrics.component'; import { PerformanceMetricsComponent } from '../components/performance-metrics.component';
import { StrategyDialogComponent } from '../dialogs/strategy-dialog.component'; import { TradesTableComponent } from '../components/trades-table.component';
import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component'; import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
import { StrategyDialogComponent } from '../dialogs/strategy-dialog.component';
@Component({ @Component({
selector: 'app-strategy-details', selector: 'app-strategy-details',
@ -34,10 +38,10 @@ import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
EquityChartComponent, EquityChartComponent,
DrawdownChartComponent, DrawdownChartComponent,
TradesTableComponent, TradesTableComponent,
PerformanceMetricsComponent PerformanceMetricsComponent,
], ],
templateUrl: './strategy-details.component.html', templateUrl: './strategy-details.component.html',
styleUrl: './strategy-details.component.css' styleUrl: './strategy-details.component.css',
}) })
export class StrategyDetailsComponent implements OnChanges { export class StrategyDetailsComponent implements OnChanges {
@Input() strategy: TradingStrategy | null = null; @Input() strategy: TradingStrategy | null = null;
@ -76,9 +80,8 @@ export class StrategyDetailsComponent implements OnChanges {
this.isLoadingSignals = true; this.isLoadingSignals = true;
// First check if we can get real signals from the API // First check if we can get real signals from the API
this.strategyService.getStrategySignals(this.strategy.id) this.strategyService.getStrategySignals(this.strategy.id).subscribe({
.subscribe({ next: response => {
next: (response) => {
if (response.success && response.data && response.data.length > 0) { if (response.success && response.data && response.data.length > 0) {
this.signals = response.data; this.signals = response.data;
} else { } else {
@ -87,12 +90,12 @@ export class StrategyDetailsComponent implements OnChanges {
} }
this.isLoadingSignals = false; this.isLoadingSignals = false;
}, },
error: (error) => { error: error => {
console.error('Error loading signals', error); console.error('Error loading signals', error);
// Fallback to mock data on error // Fallback to mock data on error
this.signals = this.generateMockSignals(); this.signals = this.generateMockSignals();
this.isLoadingSignals = false; this.isLoadingSignals = false;
} },
}); });
} }
@ -102,9 +105,8 @@ export class StrategyDetailsComponent implements OnChanges {
this.isLoadingTrades = true; this.isLoadingTrades = true;
// First check if we can get real trades from the API // First check if we can get real trades from the API
this.strategyService.getStrategyTrades(this.strategy.id) this.strategyService.getStrategyTrades(this.strategy.id).subscribe({
.subscribe({ next: response => {
next: (response) => {
if (response.success && response.data && response.data.length > 0) { if (response.success && response.data && response.data.length > 0) {
this.trades = response.data; this.trades = response.data;
} else { } else {
@ -113,12 +115,12 @@ export class StrategyDetailsComponent implements OnChanges {
} }
this.isLoadingTrades = false; this.isLoadingTrades = false;
}, },
error: (error) => { error: error => {
console.error('Error loading trades', error); console.error('Error loading trades', error);
// Fallback to mock data on error // Fallback to mock data on error
this.trades = this.generateMockTrades(); this.trades = this.generateMockTrades();
this.isLoadingTrades = false; this.isLoadingTrades = false;
} },
}); });
} }
@ -134,22 +136,20 @@ export class StrategyDetailsComponent implements OnChanges {
dailyReturn: 0.0012, dailyReturn: 0.0012,
volatility: 0.008, volatility: 0.008,
sortinoRatio: 1.2, sortinoRatio: 1.2,
calmarRatio: 0.7 calmarRatio: 0.7,
}; };
} }
listenForUpdates(): void { listenForUpdates(): void {
if (!this.strategy) return; if (!this.strategy) return;
// Subscribe to strategy signals // Subscribe to strategy signals
this.webSocketService.getStrategySignals(this.strategy.id) this.webSocketService.getStrategySignals(this.strategy.id).subscribe((signal: any) => {
.subscribe((signal: any) => {
// Add the new signal to the top of the list // Add the new signal to the top of the list
this.signals = [signal, ...this.signals.slice(0, 9)]; // Keep only the latest 10 signals this.signals = [signal, ...this.signals.slice(0, 9)]; // Keep only the latest 10 signals
}); });
// Subscribe to strategy trades // Subscribe to strategy trades
this.webSocketService.getStrategyTrades(this.strategy.id) this.webSocketService.getStrategyTrades(this.strategy.id).subscribe((trade: any) => {
.subscribe((trade: any) => {
// Add the new trade to the top of the list // Add the new trade to the top of the list
this.trades = [trade, ...this.trades.slice(0, 9)]; // Keep only the latest 10 trades this.trades = [trade, ...this.trades.slice(0, 9)]; // Keep only the latest 10 trades
@ -158,8 +158,7 @@ export class StrategyDetailsComponent implements OnChanges {
}); });
// Subscribe to strategy status updates // Subscribe to strategy status updates
this.webSocketService.getStrategyUpdates() this.webSocketService.getStrategyUpdates().subscribe((update: any) => {
.subscribe((update: any) => {
if (update.strategyId === this.strategy?.id) { if (update.strategyId === this.strategy?.id) {
// Update strategy status if changed // Update strategy status if changed
if (update.status && this.strategy && this.strategy.status !== update.status) { if (update.status && this.strategy && this.strategy.status !== update.status) {
@ -170,11 +169,11 @@ export class StrategyDetailsComponent implements OnChanges {
if (update.performance && this.strategy) { if (update.performance && this.strategy) {
this.strategy.performance = { this.strategy.performance = {
...this.strategy.performance, ...this.strategy.performance,
...update.performance ...update.performance,
}; };
this.performance = { this.performance = {
...this.performance, ...this.performance,
...update.performance ...update.performance,
}; };
} }
} }
@ -202,7 +201,7 @@ export class StrategyDetailsComponent implements OnChanges {
...currentPerformance, ...currentPerformance,
totalTrades: this.trades.length, totalTrades: this.trades.length,
winRate: winRate, winRate: winRate,
totalReturn: (currentPerformance.totalReturn || 0) + (totalPnl / 10000) // Approximate totalReturn: (currentPerformance.totalReturn || 0) + totalPnl / 10000, // Approximate
}; };
// Update strategy performance as well // Update strategy performance as well
@ -210,25 +209,32 @@ export class StrategyDetailsComponent implements OnChanges {
this.strategy.performance = { this.strategy.performance = {
...this.strategy.performance, ...this.strategy.performance,
totalTrades: this.trades.length, totalTrades: this.trades.length,
winRate: winRate winRate: winRate,
}; };
} }
} }
getStatusColor(status: string): string { getStatusColor(status: string): string {
switch (status) { switch (status) {
case 'ACTIVE': return 'green'; case 'ACTIVE':
case 'PAUSED': return 'orange'; return 'green';
case 'ERROR': return 'red'; case 'PAUSED':
default: return 'gray'; return 'orange';
case 'ERROR':
return 'red';
default:
return 'gray';
} }
} }
getSignalColor(action: string): string { getSignalColor(action: string): string {
switch (action) { switch (action) {
case 'BUY': return 'green'; case 'BUY':
case 'SELL': return 'red'; return 'green';
default: return 'gray'; case 'SELL':
return 'red';
default:
return 'gray';
} }
} }
@ -240,7 +246,7 @@ export class StrategyDetailsComponent implements OnChanges {
const dialogRef = this.dialog.open(BacktestDialogComponent, { const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px', width: '800px',
data: this.strategy data: this.strategy,
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -259,7 +265,7 @@ export class StrategyDetailsComponent implements OnChanges {
const dialogRef = this.dialog.open(StrategyDialogComponent, { const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px', width: '600px',
data: this.strategy data: this.strategy,
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -277,14 +283,14 @@ export class StrategyDetailsComponent implements OnChanges {
if (!this.strategy) return; if (!this.strategy) return;
this.strategyService.startStrategy(this.strategy.id).subscribe({ this.strategyService.startStrategy(this.strategy.id).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.strategy!.status = 'ACTIVE'; this.strategy!.status = 'ACTIVE';
} }
}, },
error: (error) => { error: error => {
console.error('Error starting strategy:', error); console.error('Error starting strategy:', error);
} },
}); });
} }
@ -295,14 +301,14 @@ export class StrategyDetailsComponent implements OnChanges {
if (!this.strategy) return; if (!this.strategy) return;
this.strategyService.pauseStrategy(this.strategy.id).subscribe({ this.strategyService.pauseStrategy(this.strategy.id).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.strategy!.status = 'PAUSED'; this.strategy!.status = 'PAUSED';
} }
}, },
error: (error) => { error: error => {
console.error('Error pausing strategy:', error); console.error('Error pausing strategy:', error);
} },
}); });
} }
@ -313,14 +319,14 @@ export class StrategyDetailsComponent implements OnChanges {
if (!this.strategy) return; if (!this.strategy) return;
this.strategyService.stopStrategy(this.strategy.id).subscribe({ this.strategyService.stopStrategy(this.strategy.id).subscribe({
next: (response) => { next: response => {
if (response.success) { if (response.success) {
this.strategy!.status = 'INACTIVE'; this.strategy!.status = 'INACTIVE';
} }
}, },
error: (error) => { error: error => {
console.error('Error stopping strategy:', error); console.error('Error stopping strategy:', error);
} },
}); });
} }
@ -333,7 +339,8 @@ export class StrategyDetailsComponent implements OnChanges {
const now = new Date(); const now = new Date();
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const symbol = this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)]; const symbol =
this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const action = actions[Math.floor(Math.random() * actions.length)]; const action = actions[Math.floor(Math.random() * actions.length)];
signals.push({ signals.push({
@ -343,7 +350,7 @@ export class StrategyDetailsComponent implements OnChanges {
confidence: 0.7 + Math.random() * 0.3, confidence: 0.7 + Math.random() * 0.3,
price: 100 + Math.random() * 50, price: 100 + Math.random() * 50,
timestamp: new Date(now.getTime() - i * 1000 * 60 * 30), // 30 min intervals timestamp: new Date(now.getTime() - i * 1000 * 60 * 30), // 30 min intervals
quantity: Math.floor(10 + Math.random() * 90) quantity: Math.floor(10 + Math.random() * 90),
}); });
} }
@ -357,7 +364,8 @@ export class StrategyDetailsComponent implements OnChanges {
const now = new Date(); const now = new Date();
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const symbol = this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)]; const symbol =
this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const entryPrice = 100 + Math.random() * 50; const entryPrice = 100 + Math.random() * 50;
const exitPrice = entryPrice * (1 + (Math.random() * 0.1 - 0.05)); // -5% to +5% const exitPrice = entryPrice * (1 + (Math.random() * 0.1 - 0.05)); // -5% to +5%
const quantity = Math.floor(10 + Math.random() * 90); const quantity = Math.floor(10 + Math.random() * 90);
@ -372,7 +380,7 @@ export class StrategyDetailsComponent implements OnChanges {
exitTime: new Date(now.getTime() - i * 1000 * 60 * 60), exitTime: new Date(now.getTime() - i * 1000 * 60 * 60),
quantity, quantity,
pnl, pnl,
pnlPercent: ((exitPrice - entryPrice) / entryPrice) * 100 pnlPercent: ((exitPrice - entryPrice) / entryPrice) * 100,
}); });
} }

View file

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
export interface RiskThresholds { export interface RiskThresholds {
@ -27,13 +27,13 @@ export interface MarketData {
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ApiService { export class ApiService {
private readonly baseUrls = { private readonly baseUrls = {
riskGuardian: 'http://localhost:3002', riskGuardian: 'http://localhost:3002',
strategyOrchestrator: 'http://localhost:3003', strategyOrchestrator: 'http://localhost:3003',
marketDataGateway: 'http://localhost:3001' marketDataGateway: 'http://localhost:3001',
}; };
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
@ -45,7 +45,9 @@ export class ApiService {
); );
} }
updateRiskThresholds(thresholds: RiskThresholds): Observable<{ success: boolean; data: RiskThresholds }> { updateRiskThresholds(
thresholds: RiskThresholds
): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.put<{ success: boolean; data: RiskThresholds }>( return this.http.put<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`, `${this.baseUrls.riskGuardian}/api/risk/thresholds`,
thresholds thresholds
@ -84,7 +86,9 @@ export class ApiService {
); );
} }
// Market Data Gateway API // Market Data Gateway API
getMarketData(symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']): Observable<{ success: boolean; data: MarketData[] }> { getMarketData(
symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']
): Observable<{ success: boolean; data: MarketData[] }> {
const symbolsParam = symbols.join(','); const symbolsParam = symbols.join(',');
return this.http.get<{ success: boolean; data: MarketData[] }>( return this.http.get<{ success: boolean; data: MarketData[] }>(
`${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}` `${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}`
@ -92,7 +96,9 @@ export class ApiService {
} }
// Health checks // Health checks
checkServiceHealth(service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'): Observable<any> { checkServiceHealth(
service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'
): Observable<any> {
return this.http.get(`${this.baseUrls[service]}/health`); return this.http.get(`${this.baseUrls[service]}/health`);
} }
} }

View file

@ -1,7 +1,7 @@
import { Injectable, signal, inject } from '@angular/core'; import { inject, Injectable, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { WebSocketService, RiskAlert } from './websocket.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { RiskAlert, WebSocketService } from './websocket.service';
export interface Notification { export interface Notification {
id: string; id: string;
@ -13,7 +13,7 @@ export interface Notification {
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class NotificationService { export class NotificationService {
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
@ -34,9 +34,9 @@ export class NotificationService {
next: (alert: RiskAlert) => { next: (alert: RiskAlert) => {
this.handleRiskAlert(alert); this.handleRiskAlert(alert);
}, },
error: (err) => { error: err => {
console.error('Risk alert subscription error:', err); console.error('Risk alert subscription error:', err);
} },
}); });
} }
@ -47,7 +47,7 @@ export class NotificationService {
title: `Risk Alert: ${alert.symbol}`, title: `Risk Alert: ${alert.symbol}`,
message: alert.message, message: alert.message,
timestamp: new Date(alert.timestamp), timestamp: new Date(alert.timestamp),
read: false read: false,
}; };
this.addNotification(notification); this.addNotification(notification);
@ -56,10 +56,14 @@ export class NotificationService {
private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' { private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' {
switch (severity) { switch (severity) {
case 'HIGH': return 'error'; case 'HIGH':
case 'MEDIUM': return 'warning'; return 'error';
case 'LOW': return 'info'; case 'MEDIUM':
default: return 'info'; return 'warning';
case 'LOW':
return 'info';
default:
return 'info';
} }
} }
@ -67,14 +71,10 @@ export class NotificationService {
const actionText = notification.type === 'error' ? 'Review' : 'Dismiss'; const actionText = notification.type === 'error' ? 'Review' : 'Dismiss';
const duration = notification.type === 'error' ? 10000 : 5000; const duration = notification.type === 'error' ? 10000 : 5000;
this.snackBar.open( this.snackBar.open(`${notification.title}: ${notification.message}`, actionText, {
`${notification.title}: ${notification.message}`,
actionText,
{
duration, duration,
panelClass: [`snack-${notification.type}`] panelClass: [`snack-${notification.type}`],
} });
);
} }
// Public methods // Public methods
@ -87,9 +87,7 @@ export class NotificationService {
markAsRead(notificationId: string) { markAsRead(notificationId: string) {
const current = this.notifications(); const current = this.notifications();
const updated = current.map(n => const updated = current.map(n => (n.id === notificationId ? { ...n, read: true } : n));
n.id === notificationId ? { ...n, read: true } : n
);
this.notifications.set(updated); this.notifications.set(updated);
this.updateUnreadCount(); this.updateUnreadCount();
} }
@ -126,12 +124,12 @@ export class NotificationService {
title, title,
message, message,
timestamp: new Date(), timestamp: new Date(),
read: false read: false,
}; };
this.addNotification(notification); this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', { this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 3000, duration: 3000,
panelClass: ['snack-success'] panelClass: ['snack-success'],
}); });
} }
@ -142,12 +140,12 @@ export class NotificationService {
title, title,
message, message,
timestamp: new Date(), timestamp: new Date(),
read: false read: false,
}; };
this.addNotification(notification); this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', { this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 8000, duration: 8000,
panelClass: ['snack-error'] panelClass: ['snack-error'],
}); });
} }
@ -158,12 +156,12 @@ export class NotificationService {
title, title,
message, message,
timestamp: new Date(), timestamp: new Date(),
read: false read: false,
}; };
this.addNotification(notification); this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', { this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 5000, duration: 5000,
panelClass: ['snack-warning'] panelClass: ['snack-warning'],
}); });
} }
@ -174,12 +172,12 @@ export class NotificationService {
title, title,
message, message,
timestamp: new Date(), timestamp: new Date(),
read: false read: false,
}; };
this.addNotification(notification); this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', { this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 4000, duration: 4000,
panelClass: ['snack-info'] panelClass: ['snack-info'],
}); });
} }

View file

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
export interface TradingStrategy { export interface TradingStrategy {
@ -80,7 +80,7 @@ interface ApiResponse<T> {
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class StrategyService { export class StrategyService {
private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint
@ -100,20 +100,35 @@ export class StrategyService {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy); return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy);
} }
updateStrategy(id: string, updates: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> { updateStrategy(
return this.http.put<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`, updates); id: string,
updates: Partial<TradingStrategy>
): Observable<ApiResponse<TradingStrategy>> {
return this.http.put<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}`,
updates
);
} }
startStrategy(id: string): Observable<ApiResponse<TradingStrategy>> { startStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/start`, {}); return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/start`,
{}
);
} }
stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> { stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/stop`, {}); return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/stop`,
{}
);
} }
pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> { pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/pause`, {}); return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/pause`,
{}
);
} }
// Backtest Management // Backtest Management
@ -122,7 +137,9 @@ export class StrategyService {
} }
getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> { getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> {
return this.http.get<ApiResponse<Record<string, any>>>(`${this.apiBaseUrl}/strategy-parameters/${type}`); return this.http.get<ApiResponse<Record<string, any>>>(
`${this.apiBaseUrl}/strategy-parameters/${type}`
);
} }
runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> { runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> {
@ -143,7 +160,9 @@ export class StrategyService {
} }
// Strategy Signals and Trades // Strategy Signals and Trades
getStrategySignals(strategyId: string): Observable<ApiResponse<Array<{ getStrategySignals(strategyId: string): Observable<
ApiResponse<
Array<{
id: string; id: string;
strategyId: string; strategyId: string;
symbol: string; symbol: string;
@ -153,11 +172,15 @@ export class StrategyService {
timestamp: Date; timestamp: Date;
confidence: number; confidence: number;
metadata?: any; metadata?: any;
}>>> { }>
>
> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`); return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`);
} }
getStrategyTrades(strategyId: string): Observable<ApiResponse<Array<{ getStrategyTrades(strategyId: string): Observable<
ApiResponse<
Array<{
id: string; id: string;
strategyId: string; strategyId: string;
symbol: string; symbol: string;
@ -168,7 +191,9 @@ export class StrategyService {
quantity: number; quantity: number;
pnl: number; pnl: number;
pnlPercent: number; pnlPercent: number;
}>>> { }>
>
> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`); return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`);
} }
@ -177,13 +202,17 @@ export class StrategyService {
// Handle date formatting and parameter conversion // Handle date formatting and parameter conversion
return { return {
...formData, ...formData,
startDate: formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate, startDate:
formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate,
endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate, endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate,
strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams) strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams),
}; };
} }
private convertParameterTypes(strategyType: string, params: Record<string, any>): Record<string, any> { private convertParameterTypes(
strategyType: string,
params: Record<string, any>
): Record<string, any> {
// Convert string parameters to correct types based on strategy requirements // Convert string parameters to correct types based on strategy requirements
const result: Record<string, any> = {}; const result: Record<string, any> = {};

View file

@ -27,13 +27,13 @@ export interface RiskAlert {
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class WebSocketService { export class WebSocketService {
private readonly WS_ENDPOINTS = { private readonly WS_ENDPOINTS = {
marketData: 'ws://localhost:3001/ws', marketData: 'ws://localhost:3001/ws',
riskGuardian: 'ws://localhost:3002/ws', riskGuardian: 'ws://localhost:3002/ws',
strategyOrchestrator: 'ws://localhost:3003/ws' strategyOrchestrator: 'ws://localhost:3003/ws',
}; };
private connections = new Map<string, WebSocket>(); private connections = new Map<string, WebSocket>();
@ -44,7 +44,7 @@ export class WebSocketService {
public connectionStatus = signal<{ [key: string]: boolean }>({ public connectionStatus = signal<{ [key: string]: boolean }>({
marketData: false, marketData: false,
riskGuardian: false, riskGuardian: false,
strategyOrchestrator: false strategyOrchestrator: false,
}); });
constructor() { constructor() {
@ -68,7 +68,7 @@ export class WebSocketService {
this.updateConnectionStatus(serviceName, true); this.updateConnectionStatus(serviceName, true);
}; };
ws.onmessage = (event) => { ws.onmessage = event => {
try { try {
const message: WebSocketMessage = JSON.parse(event.data); const message: WebSocketMessage = JSON.parse(event.data);
messageSubject.next(message); messageSubject.next(message);
@ -87,14 +87,13 @@ export class WebSocketService {
}, 5000); }, 5000);
}; };
ws.onerror = (error) => { ws.onerror = error => {
console.error(`WebSocket error for ${serviceName}:`, error); console.error(`WebSocket error for ${serviceName}:`, error);
this.updateConnectionStatus(serviceName, false); this.updateConnectionStatus(serviceName, false);
}; };
this.connections.set(serviceName, ws); this.connections.set(serviceName, ws);
this.messageSubjects.set(serviceName, messageSubject); this.messageSubjects.set(serviceName, messageSubject);
} catch (error) { } catch (error) {
console.error(`Failed to connect to ${serviceName} WebSocket:`, error); console.error(`Failed to connect to ${serviceName} WebSocket:`, error);
this.updateConnectionStatus(serviceName, false); this.updateConnectionStatus(serviceName, false);
@ -157,7 +156,8 @@ export class WebSocketService {
} }
return subject.asObservable().pipe( return subject.asObservable().pipe(
filter(message => filter(
message =>
message.type === 'strategy_signal' && message.type === 'strategy_signal' &&
(!strategyId || message.data.strategyId === strategyId) (!strategyId || message.data.strategyId === strategyId)
), ),
@ -173,7 +173,8 @@ export class WebSocketService {
} }
return subject.asObservable().pipe( return subject.asObservable().pipe(
filter(message => filter(
message =>
message.type === 'strategy_trade' && message.type === 'strategy_trade' &&
(!strategyId || message.data.strategyId === strategyId) (!strategyId || message.data.strategyId === strategyId)
), ),
@ -188,11 +189,7 @@ export class WebSocketService {
throw new Error('Strategy Orchestrator WebSocket not initialized'); throw new Error('Strategy Orchestrator WebSocket not initialized');
} }
return subject.asObservable().pipe( return subject.asObservable().pipe(filter(message => message.type.startsWith('strategy_')));
filter(message =>
message.type.startsWith('strategy_')
)
);
} }
// Send messages // Send messages

View file

@ -1,6 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app'; import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig) bootstrapApplication(App, appConfig).catch(err => console.error(err));
.catch((err) => console.error(err));

View file

@ -1,20 +1,14 @@
/** /**
* Data Service - Combined live and historical data ingestion with queue-based architecture * Data Service - Combined live and historical data ingestion with queue-based architecture
*/ */
import { getLogger } from '@stock-bot/logger';
import { loadEnvVariables } from '@stock-bot/config';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { loadEnvVariables } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
import { Shutdown } from '@stock-bot/shutdown'; import { Shutdown } from '@stock-bot/shutdown';
import { initializeProxyCache } from './providers/proxy.tasks';
import { queueManager } from './services/queue.service'; import { queueManager } from './services/queue.service';
import { initializeBatchCache } from './utils/batch-helpers'; import { initializeBatchCache } from './utils/batch-helpers';
import { initializeProxyCache } from './providers/proxy.tasks'; import { healthRoutes, marketDataRoutes, proxyRoutes, queueRoutes, testRoutes } from './routes';
import {
healthRoutes,
queueRoutes,
marketDataRoutes,
proxyRoutes,
testRoutes
} from './routes';
// Load environment variables // Load environment variables
loadEnvVariables(); loadEnvVariables();

View file

@ -1,6 +1,6 @@
import { ProxyInfo } from 'libs/http/src/types'; import { ProxyInfo } from 'libs/http/src/types';
import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
// Create logger for this provider // Create logger for this provider
const logger = getLogger('proxy-provider'); const logger = getLogger('proxy-provider');
@ -15,7 +15,8 @@ const getEvery24HourCron = (): string => {
export const proxyProvider: ProviderConfig = { export const proxyProvider: ProviderConfig = {
name: 'proxy-provider', name: 'proxy-provider',
operations: {'fetch-and-check': async (payload: { sources?: string[] }) => { operations: {
'fetch-and-check': async (payload: { sources?: string[] }) => {
const { proxyService } = await import('./proxy.tasks'); const { proxyService } = await import('./proxy.tasks');
const { queueManager } = await import('../services/queue.service'); const { queueManager } = await import('../services/queue.service');
const { processItems } = await import('../utils/batch-helpers'); const { processItems } = await import('../utils/batch-helpers');
@ -32,7 +33,7 @@ export const proxyProvider: ProviderConfig = {
(proxy, index) => ({ (proxy, index) => ({
proxy, proxy,
index, index,
source: 'batch-processing' source: 'batch-processing',
}), }),
queueManager, queueManager,
{ {
@ -41,14 +42,15 @@ export const proxyProvider: ProviderConfig = {
useBatching: process.env.PROXY_DIRECT_MODE !== 'true', useBatching: process.env.PROXY_DIRECT_MODE !== 'true',
priority: 2, priority: 2,
provider: 'proxy-provider', provider: 'proxy-provider',
operation: 'check-proxy' operation: 'check-proxy',
} }
);return { );
return {
proxiesFetched: result.totalItems, proxiesFetched: result.totalItems,
jobsCreated: result.jobsCreated, jobsCreated: result.jobsCreated,
mode: result.mode, mode: result.mode,
batchesCreated: result.batchesCreated, batchesCreated: result.batchesCreated,
processingTimeMs: result.duration processingTimeMs: result.duration,
}; };
}, },
'process-batch-items': async (payload: any) => { 'process-batch-items': async (payload: any) => {
@ -60,11 +62,11 @@ export const proxyProvider: ProviderConfig = {
}, },
'check-proxy': async (payload: { 'check-proxy': async (payload: {
proxy: ProxyInfo, proxy: ProxyInfo;
source?: string, source?: string;
batchIndex?: number, batchIndex?: number;
itemIndex?: number, itemIndex?: number;
total?: number total?: number;
}) => { }) => {
const { checkProxy } = await import('./proxy.tasks'); const { checkProxy } = await import('./proxy.tasks');
@ -75,7 +77,7 @@ export const proxyProvider: ProviderConfig = {
proxy: `${payload.proxy.host}:${payload.proxy.port}`, proxy: `${payload.proxy.host}:${payload.proxy.port}`,
isWorking: result.isWorking, isWorking: result.isWorking,
responseTime: result.responseTime, responseTime: result.responseTime,
batchIndex: payload.batchIndex batchIndex: payload.batchIndex,
}); });
return { return {
@ -87,15 +89,15 @@ export const proxyProvider: ProviderConfig = {
batchIndex: payload.batchIndex, batchIndex: payload.batchIndex,
itemIndex: payload.itemIndex, itemIndex: payload.itemIndex,
total: payload.total, total: payload.total,
source: payload.source source: payload.source,
} },
}) }),
}; };
} catch (error) { } catch (error) {
logger.warn('Proxy validation failed', { logger.warn('Proxy validation failed', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`, proxy: `${payload.proxy.host}:${payload.proxy.port}`,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
batchIndex: payload.batchIndex batchIndex: payload.batchIndex,
}); });
return { return {
@ -107,12 +109,12 @@ export const proxyProvider: ProviderConfig = {
batchIndex: payload.batchIndex, batchIndex: payload.batchIndex,
itemIndex: payload.itemIndex, itemIndex: payload.itemIndex,
total: payload.total, total: payload.total,
source: payload.source source: payload.source,
} },
}) }),
}; };
} }
} },
}, },
scheduledJobs: [ scheduledJobs: [
{ {
@ -123,9 +125,7 @@ export const proxyProvider: ProviderConfig = {
cronPattern: getEvery24HourCron(), cronPattern: getEvery24HourCron(),
priority: 5, priority: 5,
immediately: true, // Don't run immediately during startup to avoid conflicts immediately: true, // Don't run immediately during startup to avoid conflicts
description: 'Fetch and validate proxy list from sources' description: 'Fetch and validate proxy list from sources',
} },
] ],
}; };

View file

@ -1,7 +1,7 @@
import { getLogger } from '@stock-bot/logger'; import pLimit from 'p-limit';
import { createCache, type CacheProvider } from '@stock-bot/cache'; import { createCache, type CacheProvider } from '@stock-bot/cache';
import { HttpClient, ProxyInfo } from '@stock-bot/http'; import { HttpClient, ProxyInfo } from '@stock-bot/http';
import pLimit from 'p-limit'; import { getLogger } from '@stock-bot/logger';
// Type definitions // Type definitions
export interface ProxySource { export interface ProxySource {
@ -24,34 +24,134 @@ const PROXY_CONFIG = {
CHECK_URL: 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955', CHECK_URL: 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955',
CONCURRENCY_LIMIT: 100, CONCURRENCY_LIMIT: 100,
PROXY_SOURCES: [ PROXY_SOURCES: [
{id: 'prxchk', url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt', protocol: 'http'}, {
{id: 'casals', url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http', protocol: 'http'}, id: 'prxchk',
{id: 'sunny9577', url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt', protocol: 'http'}, url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt',
{id: 'themiralay', url: 'https://raw.githubusercontent.com/themiralay/Proxy-List-World/refs/heads/master/data.txt', protocol: 'http'}, protocol: 'http',
{id: 'casa-ls', url: 'https://raw.githubusercontent.com/casa-ls/proxy-list/refs/heads/main/http', protocol: 'http'}, },
{id: 'databay', url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/http.txt', protocol: 'http'}, {
{id: 'speedx', url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt', protocol: 'http'}, id: 'casals',
{id: 'monosans', url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt', protocol: 'http'}, url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http',
protocol: 'http',
},
{
id: 'sunny9577',
url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt',
protocol: 'http',
},
{
id: 'themiralay',
url: 'https://raw.githubusercontent.com/themiralay/Proxy-List-World/refs/heads/master/data.txt',
protocol: 'http',
},
{
id: 'casa-ls',
url: 'https://raw.githubusercontent.com/casa-ls/proxy-list/refs/heads/main/http',
protocol: 'http',
},
{
id: 'databay',
url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/http.txt',
protocol: 'http',
},
{
id: 'speedx',
url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt',
protocol: 'http',
},
{
id: 'monosans',
url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',
protocol: 'http',
},
{id: 'murong', url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt', protocol: 'http'}, {
{id: 'vakhov-fresh', url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt', protocol: 'http'}, id: 'murong',
{id: 'kangproxy', url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/http/http.txt', protocol: 'http'}, url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt',
{id: 'gfpcom', url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/http.txt', protocol: 'http'}, protocol: 'http',
{id: 'dpangestuw', url: 'https://raw.githubusercontent.com/dpangestuw/Free-Proxy/refs/heads/main/http_proxies.txt', protocol: 'http'}, },
{id: 'gitrecon', url: 'https://raw.githubusercontent.com/gitrecon1455/fresh-proxy-list/refs/heads/main/proxylist.txt', protocol: 'http'}, {
{id: 'vakhov-master', url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/http.txt', protocol: 'http'}, id: 'vakhov-fresh',
{id: 'breaking-tech', url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt', protocol: 'http'}, url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt',
{id: 'ercindedeoglu', url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt', protocol: 'http'}, protocol: 'http',
{id: 'tuanminpay', url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt', protocol: 'http'}, },
{
id: 'kangproxy',
url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/http/http.txt',
protocol: 'http',
},
{
id: 'gfpcom',
url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/http.txt',
protocol: 'http',
},
{
id: 'dpangestuw',
url: 'https://raw.githubusercontent.com/dpangestuw/Free-Proxy/refs/heads/main/http_proxies.txt',
protocol: 'http',
},
{
id: 'gitrecon',
url: 'https://raw.githubusercontent.com/gitrecon1455/fresh-proxy-list/refs/heads/main/proxylist.txt',
protocol: 'http',
},
{
id: 'vakhov-master',
url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/http.txt',
protocol: 'http',
},
{
id: 'breaking-tech',
url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt',
protocol: 'http',
},
{
id: 'ercindedeoglu',
url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt',
protocol: 'http',
},
{
id: 'tuanminpay',
url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt',
protocol: 'http',
},
{id: 'r00tee-https', url: 'https://raw.githubusercontent.com/r00tee/Proxy-List/refs/heads/main/Https.txt', protocol: 'https'}, {
{id: 'ercindedeoglu-https', url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt', protocol: 'https'}, id: 'r00tee-https',
{id: 'vakhov-fresh-https', url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/https.txt', protocol: 'https'}, url: 'https://raw.githubusercontent.com/r00tee/Proxy-List/refs/heads/main/Https.txt',
{id: 'databay-https', url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/https.txt', protocol: 'https'}, protocol: 'https',
{id: 'kangproxy-https', url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/https/https.txt', protocol: 'https'}, },
{id: 'zloi-user-https', url: 'https://raw.githubusercontent.com/zloi-user/hideip.me/refs/heads/master/https.txt', protocol: 'https'}, {
{id: 'gfpcom-https', url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/https.txt', protocol: 'https'}, id: 'ercindedeoglu-https',
] url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',
protocol: 'https',
},
{
id: 'vakhov-fresh-https',
url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/https.txt',
protocol: 'https',
},
{
id: 'databay-https',
url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/https.txt',
protocol: 'https',
},
{
id: 'kangproxy-https',
url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/https/https.txt',
protocol: 'https',
},
{
id: 'zloi-user-https',
url: 'https://raw.githubusercontent.com/zloi-user/hideip.me/refs/heads/master/https.txt',
protocol: 'https',
},
{
id: 'gfpcom-https',
url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/https.txt',
protocol: 'https',
},
],
}; };
// Shared instances (module-scoped, not global) // Shared instances (module-scoped, not global)
@ -68,20 +168,17 @@ let proxyStats: ProxySource[] = PROXY_CONFIG.PROXY_SOURCES.map(source => ({
url: source.url, url: source.url,
})); }));
// make a function that takes in source id and a boolean success and updates the proxyStats array // make a function that takes in source id and a boolean success and updates the proxyStats array
async function updateProxyStats(sourceId: string, success: boolean) { async function updateProxyStats(sourceId: string, success: boolean) {
const source = proxyStats.find(s => s.id === sourceId); const source = proxyStats.find(s => s.id === sourceId);
if (source !== undefined) { if (source !== undefined) {
if(typeof source.working !== 'number') if (typeof source.working !== 'number') source.working = 0;
source.working = 0; if (typeof source.total !== 'number') source.total = 0;
if(typeof source.total !== 'number')
source.total = 0;
source.total += 1; source.total += 1;
if (success) { if (success) {
source.working += 1; source.working += 1;
} }
source.percentWorking = source.working / source.total * 100; source.percentWorking = (source.working / source.total) * 100;
source.lastChecked = new Date(); source.lastChecked = new Date();
await cache.set(`${PROXY_CONFIG.CACHE_STATS_KEY}:${source.id}`, source, PROXY_CONFIG.CACHE_TTL); await cache.set(`${PROXY_CONFIG.CACHE_STATS_KEY}:${source.id}`, source, PROXY_CONFIG.CACHE_TTL);
return source; return source;
@ -120,7 +217,7 @@ async function updateProxyInCache(proxy: ProxyInfo, isWorking: boolean): Promise
// For failed proxies, only update if they already exist // For failed proxies, only update if they already exist
if (!isWorking && !existing) { if (!isWorking && !existing) {
logger.debug('Proxy not in cache, skipping failed update', { logger.debug('Proxy not in cache, skipping failed update', {
proxy: `${proxy.host}:${proxy.port}` proxy: `${proxy.host}:${proxy.port}`,
}); });
return; return;
} }
@ -132,8 +229,9 @@ async function updateProxyInCache(proxy: ProxyInfo, isWorking: boolean): Promise
const existingTotal = existing?.total || 0; const existingTotal = existing?.total || 0;
// Calculate weighted average: (existing_avg * existing_count + new_response) / (existing_count + 1) // Calculate weighted average: (existing_avg * existing_count + new_response) / (existing_count + 1)
newAverageResponseTime = existingTotal > 0 newAverageResponseTime =
? ((existingAvg * existingTotal) + proxy.responseTime) / (existingTotal + 1) existingTotal > 0
? (existingAvg * existingTotal + proxy.responseTime) / (existingTotal + 1)
: proxy.responseTime; : proxy.responseTime;
} }
@ -142,13 +240,15 @@ async function updateProxyInCache(proxy: ProxyInfo, isWorking: boolean): Promise
...existing, ...existing,
...proxy, // Keep latest proxy info ...proxy, // Keep latest proxy info
total: (existing?.total || 0) + 1, total: (existing?.total || 0) + 1,
working: isWorking ? (existing?.working || 0) + 1 : (existing?.working || 0), working: isWorking ? (existing?.working || 0) + 1 : existing?.working || 0,
isWorking, isWorking,
lastChecked: new Date(), lastChecked: new Date(),
// Add firstSeen only for new entries // Add firstSeen only for new entries
...(existing ? {} : { firstSeen: new Date() }), ...(existing ? {} : { firstSeen: new Date() }),
// Update average response time if we calculated a new one // Update average response time if we calculated a new one
...(newAverageResponseTime !== undefined ? { averageResponseTime: newAverageResponseTime } : {}) ...(newAverageResponseTime !== undefined
? { averageResponseTime: newAverageResponseTime }
: {}),
}; };
// Calculate success rate // Calculate success rate
@ -163,13 +263,14 @@ async function updateProxyInCache(proxy: ProxyInfo, isWorking: boolean): Promise
working: updated.working, working: updated.working,
total: updated.total, total: updated.total,
successRate: updated.successRate.toFixed(1) + '%', successRate: updated.successRate.toFixed(1) + '%',
avgResponseTime: updated.averageResponseTime ? `${updated.averageResponseTime.toFixed(0)}ms` : 'N/A' avgResponseTime: updated.averageResponseTime
? `${updated.averageResponseTime.toFixed(0)}ms`
: 'N/A',
}); });
} catch (error) { } catch (error) {
logger.error('Failed to update proxy in cache', { logger.error('Failed to update proxy in cache', {
proxy: `${proxy.host}:${proxy.port}`, proxy: `${proxy.host}:${proxy.port}`,
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error),
}); });
} }
} }
@ -183,7 +284,7 @@ export async function initializeProxyCache(): Promise<void> {
cache = createCache({ cache = createCache({
keyPrefix: 'proxy:', keyPrefix: 'proxy:',
ttl: PROXY_CONFIG.CACHE_TTL, ttl: PROXY_CONFIG.CACHE_TTL,
enableMetrics: true enableMetrics: true,
}); });
logger.info('Initializing proxy cache...'); logger.info('Initializing proxy cache...');
@ -204,7 +305,7 @@ async function initializeSharedResources() {
cache = createCache({ cache = createCache({
keyPrefix: 'proxy:', keyPrefix: 'proxy:',
ttl: PROXY_CONFIG.CACHE_TTL, ttl: PROXY_CONFIG.CACHE_TTL,
enableMetrics: true enableMetrics: true,
}); });
httpClient = new HttpClient({ timeout: 10000 }, logger); httpClient = new HttpClient({ timeout: 10000 }, logger);
@ -224,7 +325,7 @@ export async function queueProxyFetch(): Promise<string> {
provider: 'proxy-service', provider: 'proxy-service',
operation: 'fetch-and-check', operation: 'fetch-and-check',
payload: {}, payload: {},
priority: 5 priority: 5,
}); });
const jobId = job.id || 'unknown'; const jobId = job.id || 'unknown';
@ -241,7 +342,7 @@ export async function queueProxyCheck(proxies: ProxyInfo[]): Promise<string> {
provider: 'proxy-service', provider: 'proxy-service',
operation: 'check-specific', operation: 'check-specific',
payload: { proxies }, payload: { proxies },
priority: 3 priority: 3,
}); });
const jobId = job.id || 'unknown'; const jobId = job.id || 'unknown';
@ -285,7 +386,7 @@ export async function fetchProxiesFromSource(source: ProxySource): Promise<Proxy
logger.info(`Fetching proxies from ${source.url}`); logger.info(`Fetching proxies from ${source.url}`);
const response = await httpClient.get(source.url, { const response = await httpClient.get(source.url, {
timeout: 10000 timeout: 10000,
}); });
if (response.status !== 200) { if (response.status !== 200) {
@ -308,7 +409,7 @@ export async function fetchProxiesFromSource(source: ProxySource): Promise<Proxy
source: source.id, source: source.id,
protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5', protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5',
host: parts[0], host: parts[0],
port: parseInt(parts[1]) port: parseInt(parts[1]),
}; };
if (!isNaN(proxy.port) && proxy.host) { if (!isNaN(proxy.port) && proxy.host) {
@ -318,7 +419,6 @@ export async function fetchProxiesFromSource(source: ProxySource): Promise<Proxy
} }
logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`); logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`);
} catch (error) { } catch (error) {
logger.error(`Error fetching proxies from ${source.url}`, error); logger.error(`Error fetching proxies from ${source.url}`, error);
return []; return [];
@ -344,7 +444,7 @@ export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
// Test the proxy // Test the proxy
const response = await httpClient.get(PROXY_CONFIG.CHECK_URL, { const response = await httpClient.get(PROXY_CONFIG.CHECK_URL, {
proxy, proxy,
timeout: PROXY_CONFIG.CHECK_TIMEOUT timeout: PROXY_CONFIG.CHECK_TIMEOUT,
}); });
const isWorking = response.status >= 200 && response.status < 300; const isWorking = response.status >= 200 && response.status < 300;
@ -379,7 +479,7 @@ export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
...proxy, ...proxy,
isWorking: false, isWorking: false,
error: errorMessage, error: errorMessage,
lastChecked: new Date() lastChecked: new Date(),
}; };
// Update cache for failed proxy (increment total, don't update TTL) // Update cache for failed proxy (increment total, don't update TTL)
@ -392,7 +492,7 @@ export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
logger.debug('Proxy check failed', { logger.debug('Proxy check failed', {
host: proxy.host, host: proxy.host,
port: proxy.port, port: proxy.port,
error: errorMessage error: errorMessage,
}); });
return result; return result;

View file

@ -1,11 +1,12 @@
import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
const logger = getLogger('quotemedia-provider'); const logger = getLogger('quotemedia-provider');
export const quotemediaProvider: ProviderConfig = { export const quotemediaProvider: ProviderConfig = {
name: 'quotemedia', name: 'quotemedia',
operations: { 'live-data': async (payload: { symbol: string; fields?: string[] }) => { operations: {
'live-data': async (payload: { symbol: string; fields?: string[] }) => {
logger.info('Fetching live data from QuoteMedia', { symbol: payload.symbol }); logger.info('Fetching live data from QuoteMedia', { symbol: payload.symbol });
// Simulate QuoteMedia API call // Simulate QuoteMedia API call
@ -17,7 +18,7 @@ export const quotemediaProvider: ProviderConfig = {
changePercent: (Math.random() - 0.5) * 5, changePercent: (Math.random() - 0.5) * 5,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
source: 'quotemedia', source: 'quotemedia',
fields: payload.fields || ['price', 'volume', 'change'] fields: payload.fields || ['price', 'volume', 'change'],
}; };
// Simulate network delay // Simulate network delay
@ -31,16 +32,19 @@ export const quotemediaProvider: ProviderConfig = {
from: Date; from: Date;
to: Date; to: Date;
interval?: string; interval?: string;
fields?: string[]; }) => { fields?: string[];
}) => {
logger.info('Fetching historical data from QuoteMedia', { logger.info('Fetching historical data from QuoteMedia', {
symbol: payload.symbol, symbol: payload.symbol,
from: payload.from, from: payload.from,
to: payload.to, to: payload.to,
interval: payload.interval || '1d' interval: payload.interval || '1d',
}); });
// Generate mock historical data // Generate mock historical data
const days = Math.ceil((payload.to.getTime() - payload.from.getTime()) / (1000 * 60 * 60 * 24)); const days = Math.ceil(
(payload.to.getTime() - payload.from.getTime()) / (1000 * 60 * 60 * 24)
);
const data = []; const data = [];
for (let i = 0; i < Math.min(days, 100); i++) { for (let i = 0; i < Math.min(days, 100); i++) {
@ -52,7 +56,7 @@ export const quotemediaProvider: ProviderConfig = {
low: Math.random() * 1000 + 100, low: Math.random() * 1000 + 100,
close: Math.random() * 1000 + 100, close: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000), volume: Math.floor(Math.random() * 1000000),
source: 'quotemedia' source: 'quotemedia',
}); });
} }
@ -64,13 +68,13 @@ export const quotemediaProvider: ProviderConfig = {
interval: payload.interval || '1d', interval: payload.interval || '1d',
data, data,
source: 'quotemedia', source: 'quotemedia',
totalRecords: data.length totalRecords: data.length,
}; };
}, },
'batch-quotes': async (payload: { symbols: string[]; fields?: string[] }) => { 'batch-quotes': async (payload: { symbols: string[]; fields?: string[] }) => {
logger.info('Fetching batch quotes from QuoteMedia', { logger.info('Fetching batch quotes from QuoteMedia', {
symbols: payload.symbols, symbols: payload.symbols,
count: payload.symbols.length count: payload.symbols.length,
}); });
const quotes = payload.symbols.map(symbol => ({ const quotes = payload.symbols.map(symbol => ({
@ -79,7 +83,7 @@ export const quotemediaProvider: ProviderConfig = {
volume: Math.floor(Math.random() * 1000000), volume: Math.floor(Math.random() * 1000000),
change: (Math.random() - 0.5) * 20, change: (Math.random() - 0.5) * 20,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
source: 'quotemedia' source: 'quotemedia',
})); }));
// Simulate network delay // Simulate network delay
@ -89,9 +93,10 @@ export const quotemediaProvider: ProviderConfig = {
quotes, quotes,
source: 'quotemedia', source: 'quotemedia',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
totalSymbols: payload.symbols.length totalSymbols: payload.symbols.length,
}; };
}, 'company-profile': async (payload: { symbol: string }) => { },
'company-profile': async (payload: { symbol: string }) => {
logger.info('Fetching company profile from QuoteMedia', { symbol: payload.symbol }); logger.info('Fetching company profile from QuoteMedia', { symbol: payload.symbol });
// Simulate company profile data // Simulate company profile data
@ -104,16 +109,17 @@ export const quotemediaProvider: ProviderConfig = {
marketCap: Math.floor(Math.random() * 1000000000000), marketCap: Math.floor(Math.random() * 1000000000000),
employees: Math.floor(Math.random() * 100000), employees: Math.floor(Math.random() * 100000),
website: `https://www.${payload.symbol.toLowerCase()}.com`, website: `https://www.${payload.symbol.toLowerCase()}.com`,
source: 'quotemedia' source: 'quotemedia',
}; };
await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 100)); await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 100));
return profile; return profile;
}, 'options-chain': async (payload: { symbol: string; expiration?: string }) => { },
'options-chain': async (payload: { symbol: string; expiration?: string }) => {
logger.info('Fetching options chain from QuoteMedia', { logger.info('Fetching options chain from QuoteMedia', {
symbol: payload.symbol, symbol: payload.symbol,
expiration: payload.expiration expiration: payload.expiration,
}); });
// Generate mock options data // Generate mock options data
@ -123,7 +129,7 @@ export const quotemediaProvider: ProviderConfig = {
bid: Math.random() * 10, bid: Math.random() * 10,
ask: Math.random() * 10 + 0.5, ask: Math.random() * 10 + 0.5,
volume: Math.floor(Math.random() * 1000), volume: Math.floor(Math.random() * 1000),
openInterest: Math.floor(Math.random() * 5000) openInterest: Math.floor(Math.random() * 5000),
})); }));
const puts = strikes.map(strike => ({ const puts = strikes.map(strike => ({
@ -131,18 +137,20 @@ export const quotemediaProvider: ProviderConfig = {
bid: Math.random() * 10, bid: Math.random() * 10,
ask: Math.random() * 10 + 0.5, ask: Math.random() * 10 + 0.5,
volume: Math.floor(Math.random() * 1000), volume: Math.floor(Math.random() * 1000),
openInterest: Math.floor(Math.random() * 5000) openInterest: Math.floor(Math.random() * 5000),
})); }));
await new Promise(resolve => setTimeout(resolve, 400 + Math.random() * 300)); await new Promise(resolve => setTimeout(resolve, 400 + Math.random() * 300));
return { return {
symbol: payload.symbol, symbol: payload.symbol,
expiration: payload.expiration || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], expiration:
payload.expiration ||
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
calls, calls,
puts, puts,
source: 'quotemedia' source: 'quotemedia',
}; };
} },
}, },
scheduledJobs: [ scheduledJobs: [
@ -170,5 +178,5 @@ export const quotemediaProvider: ProviderConfig = {
// priority: 3, // priority: 3,
// description: 'Update company profile data' // description: 'Update company profile data'
// } // }
] ],
}; };

View file

@ -1,5 +1,5 @@
import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { ProviderConfig } from '../services/provider-registry.service';
const logger = getLogger('yahoo-provider'); const logger = getLogger('yahoo-provider');
@ -7,8 +7,6 @@ export const yahooProvider: ProviderConfig = {
name: 'yahoo-finance', name: 'yahoo-finance',
operations: { operations: {
'live-data': async (payload: { symbol: string; modules?: string[] }) => { 'live-data': async (payload: { symbol: string; modules?: string[] }) => {
logger.info('Fetching live data from Yahoo Finance', { symbol: payload.symbol }); logger.info('Fetching live data from Yahoo Finance', { symbol: payload.symbol });
// Simulate Yahoo Finance API call // Simulate Yahoo Finance API call
@ -27,7 +25,7 @@ export const yahooProvider: ProviderConfig = {
fiftyTwoWeekLow: Math.random() * 800 + 50, fiftyTwoWeekLow: Math.random() * 800 + 50,
timestamp: Date.now() / 1000, timestamp: Date.now() / 1000,
source: 'yahoo-finance', source: 'yahoo-finance',
modules: payload.modules || ['price', 'summaryDetail'] modules: payload.modules || ['price', 'summaryDetail'],
}; };
// Simulate network delay // Simulate network delay
@ -41,7 +39,8 @@ export const yahooProvider: ProviderConfig = {
period1: number; period1: number;
period2: number; period2: number;
interval?: string; interval?: string;
events?: string; }) => { events?: string;
}) => {
const { getLogger } = await import('@stock-bot/logger'); const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider'); const logger = getLogger('yahoo-provider');
@ -49,7 +48,7 @@ export const yahooProvider: ProviderConfig = {
symbol: payload.symbol, symbol: payload.symbol,
period1: payload.period1, period1: payload.period1,
period2: payload.period2, period2: payload.period2,
interval: payload.interval || '1d' interval: payload.interval || '1d',
}); });
// Generate mock historical data // Generate mock historical data
@ -67,7 +66,7 @@ export const yahooProvider: ProviderConfig = {
close: Math.random() * 1000 + 100, close: Math.random() * 1000 + 100,
adjClose: Math.random() * 1000 + 100, adjClose: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000), volume: Math.floor(Math.random() * 1000000),
source: 'yahoo-finance' source: 'yahoo-finance',
}); });
} }
@ -79,22 +78,26 @@ export const yahooProvider: ProviderConfig = {
interval: payload.interval || '1d', interval: payload.interval || '1d',
timestamps: data.map(d => d.timestamp), timestamps: data.map(d => d.timestamp),
indicators: { indicators: {
quote: [{ quote: [
{
open: data.map(d => d.open), open: data.map(d => d.open),
high: data.map(d => d.high), high: data.map(d => d.high),
low: data.map(d => d.low), low: data.map(d => d.low),
close: data.map(d => d.close), close: data.map(d => d.close),
volume: data.map(d => d.volume) volume: data.map(d => d.volume),
}], },
adjclose: [{ ],
adjclose: data.map(d => d.adjClose) adjclose: [
}] {
adjclose: data.map(d => d.adjClose),
},
],
}, },
source: 'yahoo-finance', source: 'yahoo-finance',
totalRecords: data.length totalRecords: data.length,
}; };
}, },
'search': async (payload: { query: string; quotesCount?: number; newsCount?: number }) => { search: async (payload: { query: string; quotesCount?: number; newsCount?: number }) => {
const { getLogger } = await import('@stock-bot/logger'); const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider'); const logger = getLogger('yahoo-provider');
@ -107,7 +110,7 @@ export const yahooProvider: ProviderConfig = {
longname: `${payload.query} Corporation ${i}`, longname: `${payload.query} Corporation ${i}`,
exchDisp: 'NASDAQ', exchDisp: 'NASDAQ',
typeDisp: 'Equity', typeDisp: 'Equity',
source: 'yahoo-finance' source: 'yahoo-finance',
})); }));
const news = Array.from({ length: payload.newsCount || 3 }, (_, i) => ({ const news = Array.from({ length: payload.newsCount || 3 }, (_, i) => ({
@ -116,7 +119,7 @@ export const yahooProvider: ProviderConfig = {
publisher: 'Financial News', publisher: 'Financial News',
providerPublishTime: Date.now() - i * 3600000, providerPublishTime: Date.now() - i * 3600000,
type: 'STORY', type: 'STORY',
source: 'yahoo-finance' source: 'yahoo-finance',
})); }));
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 200)); await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 200));
@ -126,15 +129,16 @@ export const yahooProvider: ProviderConfig = {
news, news,
totalQuotes: quotes.length, totalQuotes: quotes.length,
totalNews: news.length, totalNews: news.length,
source: 'yahoo-finance' source: 'yahoo-finance',
}; };
}, 'financials': async (payload: { symbol: string; type?: 'income' | 'balance' | 'cash' }) => { },
financials: async (payload: { symbol: string; type?: 'income' | 'balance' | 'cash' }) => {
const { getLogger } = await import('@stock-bot/logger'); const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider'); const logger = getLogger('yahoo-provider');
logger.info('Fetching financials from Yahoo Finance', { logger.info('Fetching financials from Yahoo Finance', {
symbol: payload.symbol, symbol: payload.symbol,
type: payload.type || 'income' type: payload.type || 'income',
}); });
// Generate mock financial data // Generate mock financial data
@ -147,26 +151,27 @@ export const yahooProvider: ProviderConfig = {
revenue: Math.floor(Math.random() * 100000000000), revenue: Math.floor(Math.random() * 100000000000),
netIncome: Math.floor(Math.random() * 10000000000), netIncome: Math.floor(Math.random() * 10000000000),
totalAssets: Math.floor(Math.random() * 500000000000), totalAssets: Math.floor(Math.random() * 500000000000),
totalDebt: Math.floor(Math.random() * 50000000000) totalDebt: Math.floor(Math.random() * 50000000000),
})), })),
quarterly: Array.from({ length: 4 }, (_, i) => ({ quarterly: Array.from({ length: 4 }, (_, i) => ({
fiscalQuarter: `Q${4 - i} 2024`, fiscalQuarter: `Q${4 - i} 2024`,
revenue: Math.floor(Math.random() * 25000000000), revenue: Math.floor(Math.random() * 25000000000),
netIncome: Math.floor(Math.random() * 2500000000) netIncome: Math.floor(Math.random() * 2500000000),
})), })),
source: 'yahoo-finance' source: 'yahoo-finance',
}; };
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200)); await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200));
return financials; return financials;
}, 'earnings': async (payload: { symbol: string; period?: 'annual' | 'quarterly' }) => { },
earnings: async (payload: { symbol: string; period?: 'annual' | 'quarterly' }) => {
const { getLogger } = await import('@stock-bot/logger'); const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider'); const logger = getLogger('yahoo-provider');
logger.info('Fetching earnings from Yahoo Finance', { logger.info('Fetching earnings from Yahoo Finance', {
symbol: payload.symbol, symbol: payload.symbol,
period: payload.period || 'quarterly' period: payload.period || 'quarterly',
}); });
// Generate mock earnings data // Generate mock earnings data
@ -179,15 +184,16 @@ export const yahooProvider: ProviderConfig = {
epsActual: Math.random() * 5, epsActual: Math.random() * 5,
revenueEstimate: Math.floor(Math.random() * 50000000000), revenueEstimate: Math.floor(Math.random() * 50000000000),
revenueActual: Math.floor(Math.random() * 50000000000), revenueActual: Math.floor(Math.random() * 50000000000),
surprise: (Math.random() - 0.5) * 2 surprise: (Math.random() - 0.5) * 2,
})), })),
source: 'yahoo-finance' source: 'yahoo-finance',
}; };
await new Promise(resolve => setTimeout(resolve, 250 + Math.random() * 150)); await new Promise(resolve => setTimeout(resolve, 250 + Math.random() * 150));
return earnings; return earnings;
}, 'recommendations': async (payload: { symbol: string }) => { },
recommendations: async (payload: { symbol: string }) => {
const { getLogger } = await import('@stock-bot/logger'); const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider'); const logger = getLogger('yahoo-provider');
@ -201,7 +207,7 @@ export const yahooProvider: ProviderConfig = {
buy: Math.floor(Math.random() * 15), buy: Math.floor(Math.random() * 15),
hold: Math.floor(Math.random() * 20), hold: Math.floor(Math.random() * 20),
sell: Math.floor(Math.random() * 5), sell: Math.floor(Math.random() * 5),
strongSell: Math.floor(Math.random() * 3) strongSell: Math.floor(Math.random() * 3),
}, },
trend: Array.from({ length: 4 }, (_, i) => ({ trend: Array.from({ length: 4 }, (_, i) => ({
period: `${i}m`, period: `${i}m`,
@ -209,14 +215,14 @@ export const yahooProvider: ProviderConfig = {
buy: Math.floor(Math.random() * 15), buy: Math.floor(Math.random() * 15),
hold: Math.floor(Math.random() * 20), hold: Math.floor(Math.random() * 20),
sell: Math.floor(Math.random() * 5), sell: Math.floor(Math.random() * 5),
strongSell: Math.floor(Math.random() * 3) strongSell: Math.floor(Math.random() * 3),
})), })),
source: 'yahoo-finance' source: 'yahoo-finance',
}; };
await new Promise(resolve => setTimeout(resolve, 180 + Math.random() * 120)); await new Promise(resolve => setTimeout(resolve, 180 + Math.random() * 120));
return recommendations; return recommendations;
} },
}, },
scheduledJobs: [ scheduledJobs: [
@ -244,5 +250,5 @@ export const yahooProvider: ProviderConfig = {
// priority: 6, // priority: 6,
// description: 'Check earnings data for Apple' // description: 'Check earnings data for Apple'
// } // }
] ],
}; };

View file

@ -7,14 +7,14 @@ import { queueManager } from '../services/queue.service';
export const healthRoutes = new Hono(); export const healthRoutes = new Hono();
// Health check endpoint // Health check endpoint
healthRoutes.get('/health', (c) => { healthRoutes.get('/health', c => {
return c.json({ return c.json({
service: 'data-service', service: 'data-service',
status: 'healthy', status: 'healthy',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
queue: { queue: {
status: 'running', status: 'running',
workers: queueManager.getWorkerCount() workers: queueManager.getWorkerCount(),
} },
}); });
}); });

View file

@ -10,7 +10,7 @@ const logger = getLogger('market-data-routes');
export const marketDataRoutes = new Hono(); export const marketDataRoutes = new Hono();
// Market data endpoints // Market data endpoints
marketDataRoutes.get('/api/live/:symbol', async (c) => { marketDataRoutes.get('/api/live/:symbol', async c => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
logger.info('Live data request', { symbol }); logger.info('Live data request', { symbol });
@ -21,13 +21,13 @@ marketDataRoutes.get('/api/live/:symbol', async (c) => {
service: 'market-data', service: 'market-data',
provider: 'yahoo-finance', provider: 'yahoo-finance',
operation: 'live-data', operation: 'live-data',
payload: { symbol } payload: { symbol },
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
message: 'Live data job queued', message: 'Live data job queued',
jobId: job.id, jobId: job.id,
symbol symbol,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue live data job', { symbol, error }); logger.error('Failed to queue live data job', { symbol, error });
@ -35,7 +35,7 @@ marketDataRoutes.get('/api/live/:symbol', async (c) => {
} }
}); });
marketDataRoutes.get('/api/historical/:symbol', async (c) => { marketDataRoutes.get('/api/historical/:symbol', async c => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
const from = c.req.query('from'); const from = c.req.query('from');
const to = c.req.query('to'); const to = c.req.query('to');
@ -55,8 +55,8 @@ marketDataRoutes.get('/api/historical/:symbol', async (c) => {
payload: { payload: {
symbol, symbol,
from: fromDate.toISOString(), from: fromDate.toISOString(),
to: toDate.toISOString() to: toDate.toISOString(),
} },
}); });
return c.json({ return c.json({
@ -65,7 +65,7 @@ marketDataRoutes.get('/api/historical/:symbol', async (c) => {
jobId: job.id, jobId: job.id,
symbol, symbol,
from: fromDate, from: fromDate,
to: toDate to: toDate,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue historical data job', { symbol, from, to, error }); logger.error('Failed to queue historical data job', { symbol, from, to, error });

View file

@ -10,20 +10,20 @@ const logger = getLogger('proxy-routes');
export const proxyRoutes = new Hono(); export const proxyRoutes = new Hono();
// Proxy management endpoints // Proxy management endpoints
proxyRoutes.post('/api/proxy/fetch', async (c) => { proxyRoutes.post('/api/proxy/fetch', async c => {
try { try {
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-fetch', type: 'proxy-fetch',
provider: 'proxy-provider', provider: 'proxy-provider',
operation: 'fetch-and-check', operation: 'fetch-and-check',
payload: {}, payload: {},
priority: 5 priority: 5,
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
jobId: job.id, jobId: job.id,
message: 'Proxy fetch job queued' message: 'Proxy fetch job queued',
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue proxy fetch', { error }); logger.error('Failed to queue proxy fetch', { error });
@ -31,7 +31,7 @@ proxyRoutes.post('/api/proxy/fetch', async (c) => {
} }
}); });
proxyRoutes.post('/api/proxy/check', async (c) => { proxyRoutes.post('/api/proxy/check', async c => {
try { try {
const { proxies } = await c.req.json(); const { proxies } = await c.req.json();
const job = await queueManager.addJob({ const job = await queueManager.addJob({
@ -39,13 +39,13 @@ proxyRoutes.post('/api/proxy/check', async (c) => {
provider: 'proxy-provider', provider: 'proxy-provider',
operation: 'check-specific', operation: 'check-specific',
payload: { proxies }, payload: { proxies },
priority: 8 priority: 8,
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
jobId: job.id, jobId: job.id,
message: `Proxy check job queued for ${proxies.length} proxies` message: `Proxy check job queued for ${proxies.length} proxies`,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue proxy check', { error }); logger.error('Failed to queue proxy check', { error });
@ -54,20 +54,20 @@ proxyRoutes.post('/api/proxy/check', async (c) => {
}); });
// Get proxy stats via queue // Get proxy stats via queue
proxyRoutes.get('/api/proxy/stats', async (c) => { proxyRoutes.get('/api/proxy/stats', async c => {
try { try {
const job = await queueManager.addJob({ const job = await queueManager.addJob({
type: 'proxy-stats', type: 'proxy-stats',
provider: 'proxy-provider', provider: 'proxy-provider',
operation: 'get-stats', operation: 'get-stats',
payload: {}, payload: {},
priority: 3 priority: 3,
}); });
return c.json({ return c.json({
status: 'success', status: 'success',
jobId: job.id, jobId: job.id,
message: 'Proxy stats job queued' message: 'Proxy stats job queued',
}); });
} catch (error) { } catch (error) {
logger.error('Failed to queue proxy stats', { error }); logger.error('Failed to queue proxy stats', { error });

View file

@ -10,7 +10,7 @@ const logger = getLogger('queue-routes');
export const queueRoutes = new Hono(); export const queueRoutes = new Hono();
// Queue management endpoints // Queue management endpoints
queueRoutes.get('/api/queue/status', async (c) => { queueRoutes.get('/api/queue/status', async c => {
try { try {
const status = await queueManager.getQueueStatus(); const status = await queueManager.getQueueStatus();
return c.json({ status: 'success', data: status }); return c.json({ status: 'success', data: status });
@ -20,7 +20,7 @@ queueRoutes.get('/api/queue/status', async (c) => {
} }
}); });
queueRoutes.post('/api/queue/job', async (c) => { queueRoutes.post('/api/queue/job', async c => {
try { try {
const jobData = await c.req.json(); const jobData = await c.req.json();
const job = await queueManager.addJob(jobData); const job = await queueManager.addJob(jobData);
@ -32,7 +32,7 @@ queueRoutes.post('/api/queue/job', async (c) => {
}); });
// Provider registry endpoints // Provider registry endpoints
queueRoutes.get('/api/providers', async (c) => { queueRoutes.get('/api/providers', async c => {
try { try {
const { providerRegistry } = await import('../services/provider-registry.service'); const { providerRegistry } = await import('../services/provider-registry.service');
const providers = providerRegistry.getProviders(); const providers = providerRegistry.getProviders();
@ -44,14 +44,14 @@ queueRoutes.get('/api/providers', async (c) => {
}); });
// Add new endpoint to see scheduled jobs // Add new endpoint to see scheduled jobs
queueRoutes.get('/api/scheduled-jobs', async (c) => { queueRoutes.get('/api/scheduled-jobs', async c => {
try { try {
const { providerRegistry } = await import('../services/provider-registry.service'); const { providerRegistry } = await import('../services/provider-registry.service');
const jobs = providerRegistry.getAllScheduledJobs(); const jobs = providerRegistry.getAllScheduledJobs();
return c.json({ return c.json({
status: 'success', status: 'success',
count: jobs.length, count: jobs.length,
jobs jobs,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get scheduled jobs info', { error }); logger.error('Failed to get scheduled jobs info', { error });
@ -59,7 +59,7 @@ queueRoutes.get('/api/scheduled-jobs', async (c) => {
} }
}); });
queueRoutes.post('/api/queue/drain', async (c) => { queueRoutes.post('/api/queue/drain', async c => {
try { try {
await queueManager.drainQueue(); await queueManager.drainQueue();
const status = await queueManager.getQueueStatus(); const status = await queueManager.getQueueStatus();

View file

@ -10,7 +10,7 @@ const logger = getLogger('test-routes');
export const testRoutes = new Hono(); export const testRoutes = new Hono();
// Test endpoint for new functional batch processing // Test endpoint for new functional batch processing
testRoutes.post('/api/test/batch-symbols', async (c) => { testRoutes.post('/api/test/batch-symbols', async c => {
try { try {
const { symbols, useBatching = false, totalDelayHours = 1 } = await c.req.json(); const { symbols, useBatching = false, totalDelayHours = 1 } = await c.req.json();
const { processItems } = await import('../utils/batch-helpers'); const { processItems } = await import('../utils/batch-helpers');
@ -24,7 +24,7 @@ testRoutes.post('/api/test/batch-symbols', async (c) => {
(symbol, index) => ({ (symbol, index) => ({
symbol, symbol,
index, index,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}), }),
queueManager, queueManager,
{ {
@ -33,14 +33,14 @@ testRoutes.post('/api/test/batch-symbols', async (c) => {
batchSize: 10, batchSize: 10,
priority: 1, priority: 1,
provider: 'test-provider', provider: 'test-provider',
operation: 'live-data' operation: 'live-data',
} }
); );
return c.json({ return c.json({
status: 'success', status: 'success',
message: 'Batch processing started', message: 'Batch processing started',
result result,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to start batch symbol processing', { error }); logger.error('Failed to start batch symbol processing', { error });
@ -48,7 +48,7 @@ testRoutes.post('/api/test/batch-symbols', async (c) => {
} }
}); });
testRoutes.post('/api/test/batch-custom', async (c) => { testRoutes.post('/api/test/batch-custom', async c => {
try { try {
const { items, useBatching = false, totalDelayHours = 0.5 } = await c.req.json(); const { items, useBatching = false, totalDelayHours = 0.5 } = await c.req.json();
const { processItems } = await import('../utils/batch-helpers'); const { processItems } = await import('../utils/batch-helpers');
@ -62,7 +62,7 @@ testRoutes.post('/api/test/batch-custom', async (c) => {
(item, index) => ({ (item, index) => ({
originalItem: item, originalItem: item,
processIndex: index, processIndex: index,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}), }),
queueManager, queueManager,
{ {
@ -71,14 +71,14 @@ testRoutes.post('/api/test/batch-custom', async (c) => {
batchSize: 5, batchSize: 5,
priority: 1, priority: 1,
provider: 'test-provider', provider: 'test-provider',
operation: 'custom-test' operation: 'custom-test',
} }
); );
return c.json({ return c.json({
status: 'success', status: 'success',
message: 'Custom batch processing started', message: 'Custom batch processing started',
result result,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to start custom batch processing', { error }); logger.error('Failed to start custom batch processing', { error });

View file

@ -52,7 +52,7 @@ export function createProviderRegistry(): ProviderRegistry {
providers.set(config.name, config); providers.set(config.name, config);
logger.info(`Registered provider: ${config.name}`, { logger.info(`Registered provider: ${config.name}`, {
operations: Object.keys(config.operations), operations: Object.keys(config.operations),
scheduledJobs: config.scheduledJobs?.length || 0 scheduledJobs: config.scheduledJobs?.length || 0,
}); });
} }
@ -87,7 +87,7 @@ export function createProviderRegistry(): ProviderRegistry {
for (const job of config.scheduledJobs) { for (const job of config.scheduledJobs) {
allJobs.push({ allJobs.push({
provider: config.name, provider: config.name,
job job,
}); });
} }
} }
@ -102,7 +102,7 @@ export function createProviderRegistry(): ProviderRegistry {
function getProviders(): Array<{ key: string; config: ProviderConfig }> { function getProviders(): Array<{ key: string; config: ProviderConfig }> {
return Array.from(providers.entries()).map(([key, config]) => ({ return Array.from(providers.entries()).map(([key, config]) => ({
key, key,
config config,
})); }));
} }
@ -127,7 +127,7 @@ export function createProviderRegistry(): ProviderRegistry {
getAllScheduledJobs, getAllScheduledJobs,
getProviders, getProviders,
hasProvider, hasProvider,
clear clear,
}; };
} }

View file

@ -1,4 +1,4 @@
import { Queue, Worker, QueueEvents, type Job } from 'bullmq'; import { Queue, QueueEvents, Worker, type Job } from 'bullmq';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { providerRegistry, type JobData } from './provider-registry.service'; import { providerRegistry, type JobData } from './provider-registry.service';
@ -13,8 +13,8 @@ export class QueueService {
concurrency: parseInt(process.env.WORKER_CONCURRENCY || '20'), concurrency: parseInt(process.env.WORKER_CONCURRENCY || '20'),
redis: { redis: {
host: process.env.DRAGONFLY_HOST || 'localhost', host: process.env.DRAGONFLY_HOST || 'localhost',
port: parseInt(process.env.DRAGONFLY_PORT || '6379') port: parseInt(process.env.DRAGONFLY_PORT || '6379'),
} },
}; };
private get isInitialized() { private get isInitialized() {
@ -23,7 +23,8 @@ export class QueueService {
constructor() { constructor() {
// Don't initialize in constructor to allow for proper async initialization // Don't initialize in constructor to allow for proper async initialization
} async initialize() { }
async initialize() {
if (this.isInitialized) { if (this.isInitialized) {
this.logger.warn('Queue service already initialized'); this.logger.warn('Queue service already initialized');
return; return;
@ -38,15 +39,14 @@ export class QueueService {
// Step 2: Setup queue and workers // Step 2: Setup queue and workers
const connection = this.getConnection(); const connection = this.getConnection();
const queueName = '{data-service-queue}'; const queueName = '{data-service-queue}';
this.queue = new Queue(queueName, { this.queue = new Queue(queueName, {
connection, connection,
defaultJobOptions: { defaultJobOptions: {
removeOnComplete: 10, removeOnComplete: 10,
removeOnFail: 5, removeOnFail: 5,
attempts: 3, attempts: 3,
backoff: { type: 'exponential', delay: 1000 } backoff: { type: 'exponential', delay: 1000 },
} },
}); });
this.queueEvents = new QueueEvents(queueName, { connection }); this.queueEvents = new QueueEvents(queueName, { connection });
@ -58,7 +58,7 @@ export class QueueService {
await Promise.all([ await Promise.all([
this.queue.waitUntilReady(), this.queue.waitUntilReady(),
this.queueEvents.waitUntilReady(), this.queueEvents.waitUntilReady(),
...this.workers.map(worker => worker.waitUntilReady()) ...this.workers.map(worker => worker.waitUntilReady()),
]); ]);
// Step 5: Setup events and scheduled tasks // Step 5: Setup events and scheduled tasks
@ -67,19 +67,19 @@ export class QueueService {
this.logger.info('Queue service initialized successfully', { this.logger.info('Queue service initialized successfully', {
workers: workerCount, workers: workerCount,
totalConcurrency totalConcurrency,
}); });
} catch (error) { } catch (error) {
this.logger.error('Failed to initialize queue service', { error }); this.logger.error('Failed to initialize queue service', { error });
throw error; throw error;
} }
} private getConnection() { }
private getConnection() {
return { return {
...this.config.redis, ...this.config.redis,
maxRetriesPerRequest: null, maxRetriesPerRequest: null,
retryDelayOnFailover: 100, retryDelayOnFailover: 100,
lazyConnect: false lazyConnect: false,
}; };
} }
@ -94,31 +94,57 @@ export class QueueService {
// Setup events inline // Setup events inline
worker.on('ready', () => this.logger.info(`Worker ${i + 1} ready`)); worker.on('ready', () => this.logger.info(`Worker ${i + 1} ready`));
worker.on('error', (error) => this.logger.error(`Worker ${i + 1} error`, { error })); worker.on('error', error => this.logger.error(`Worker ${i + 1} error`, { error }));
this.workers.push(worker); this.workers.push(worker);
} }
return { return {
workerCount: this.config.workers, workerCount: this.config.workers,
totalConcurrency: this.config.workers * this.config.concurrency totalConcurrency: this.config.workers * this.config.concurrency,
}; };
} private setupQueueEvents() { }
// Only log failures, not every completion private setupQueueEvents() {
this.queueEvents.on('failed', (job, error) => { // Add comprehensive logging to see job flow
this.logger.error('Job failed', { this.queueEvents.on('added', job => {
this.logger.debug('Job added to queue', {
id: job.jobId, id: job.jobId,
error: String(error)
}); });
}); });
// Only log completions in debug mode this.queueEvents.on('waiting', job => {
if (process.env.LOG_LEVEL === 'debug') { this.logger.debug('Job moved to waiting', {
this.queueEvents.on('completed', (job) => { id: job.jobId,
this.logger.debug('Job completed', { id: job.jobId }); });
});
this.queueEvents.on('active', job => {
this.logger.debug('Job became active', {
id: job.jobId,
});
});
this.queueEvents.on('delayed', job => {
this.logger.debug('Job delayed', {
id: job.jobId,
delay: job.delay,
});
});
this.queueEvents.on('completed', job => {
this.logger.debug('Job completed', {
id: job.jobId,
});
});
this.queueEvents.on('failed', (job, error) => {
this.logger.error('Job failed', {
id: job.jobId,
error: String(error),
});
}); });
} }
}private async registerProviders() { private async registerProviders() {
this.logger.info('Registering providers...'); this.logger.info('Registering providers...');
try { try {
@ -126,7 +152,7 @@ export class QueueService {
const providers = [ const providers = [
{ module: '../providers/proxy.provider', export: 'proxyProvider' }, { module: '../providers/proxy.provider', export: 'proxyProvider' },
{ module: '../providers/quotemedia.provider', export: 'quotemediaProvider' }, { module: '../providers/quotemedia.provider', export: 'quotemediaProvider' },
{ module: '../providers/yahoo.provider', export: 'yahooProvider' } { module: '../providers/yahoo.provider', export: 'yahooProvider' },
]; ];
// Import and register all providers // Import and register all providers
@ -140,15 +166,17 @@ export class QueueService {
this.logger.error('Failed to register providers', { error }); this.logger.error('Failed to register providers', { error });
throw error; throw error;
} }
}private async processJob(job: Job) { }
private async processJob(job: Job) {
const { provider, operation, payload }: JobData = job.data; const { provider, operation, payload }: JobData = job.data;
this.logger.info('Processing job', { this.logger.info('Processing job', {
id: job.id, id: job.id,
provider, provider,
operation, operation,
payloadKeys: Object.keys(payload || {}) payloadKeys: Object.keys(payload || {}),
}); try { });
try {
let result; let result;
if (operation === 'process-batch-items') { if (operation === 'process-batch-items') {
@ -169,28 +197,30 @@ export class QueueService {
this.logger.info('Job completed successfully', { this.logger.info('Job completed successfully', {
id: job.id, id: job.id,
provider, provider,
operation operation,
}); });
return result; return result;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Job failed', { this.logger.error('Job failed', {
id: job.id, id: job.id,
provider, provider,
operation, operation,
error: errorMessage error: errorMessage,
}); });
throw error; throw error;
} }
} async addBulk(jobs: any[]): Promise<any[]> { }
async addBulk(jobs: any[]): Promise<any[]> {
return await this.queue.addBulk(jobs); return await this.queue.addBulk(jobs);
} }
private getTotalConcurrency() { private getTotalConcurrency() {
return this.workers.reduce((total, worker) => total + (worker.opts.concurrency || 1), 0); return this.workers.reduce((total, worker) => total + (worker.opts.concurrency || 1), 0);
} }
private async setupScheduledTasks() { private async setupScheduledTasks() {
const allScheduledJobs = providerRegistry.getAllScheduledJobs(); const allScheduledJobs = providerRegistry.getAllScheduledJobs();
@ -204,14 +234,17 @@ export class QueueService {
// Use Promise.allSettled for parallel processing + better error handling // Use Promise.allSettled for parallel processing + better error handling
const results = await Promise.allSettled( const results = await Promise.allSettled(
allScheduledJobs.map(async ({ provider, job }) => { allScheduledJobs.map(async ({ provider, job }) => {
await this.addRecurringJob({ await this.addRecurringJob(
{
type: job.type, type: job.type,
provider, provider,
operation: job.operation, operation: job.operation,
payload: job.payload, payload: job.payload,
priority: job.priority, priority: job.priority,
immediately: job.immediately || false immediately: job.immediately || false,
}, job.cronPattern); },
job.cronPattern
);
return { provider, operation: job.operation }; return { provider, operation: job.operation };
}) })
@ -227,16 +260,17 @@ export class QueueService {
this.logger.error('Failed to register scheduled job', { this.logger.error('Failed to register scheduled job', {
provider, provider,
operation: job.operation, operation: job.operation,
error: result.reason error: result.reason,
}); });
}); });
} }
this.logger.info('Scheduled tasks setup complete', { this.logger.info('Scheduled tasks setup complete', {
successful: successful.length, successful: successful.length,
failed: failed.length failed: failed.length,
}); });
} private async addJobInternal(jobData: JobData, options: any = {}) { }
private async addJobInternal(jobData: JobData, options: any = {}) {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error('Queue service not initialized'); throw new Error('Queue service not initialized');
} }
@ -246,13 +280,15 @@ export class QueueService {
priority: jobData.priority || 0, priority: jobData.priority || 0,
removeOnComplete: 10, removeOnComplete: 10,
removeOnFail: 5, removeOnFail: 5,
...options ...options,
}); });
} }
async addJob(jobData: JobData, options?: any) { async addJob(jobData: JobData, options?: any) {
return this.addJobInternal(jobData, options); return this.addJobInternal(jobData, options);
} async addRecurringJob(jobData: JobData, cronPattern: string, options?: any) { }
async addRecurringJob(jobData: JobData, cronPattern: string, options?: any) {
const jobKey = `recurring-${jobData.provider}-${jobData.operation}`; const jobKey = `recurring-${jobData.provider}-${jobData.operation}`;
return this.addJobInternal(jobData, { return this.addJobInternal(jobData, {
@ -267,9 +303,9 @@ export class QueueService {
attempts: 2, attempts: 2,
backoff: { backoff: {
type: 'fixed', type: 'fixed',
delay: 5000 delay: 5000,
}, },
...options ...options,
}); });
} }
async getJobStats() { async getJobStats() {
@ -281,7 +317,7 @@ export class QueueService {
this.queue.getActive(), this.queue.getActive(),
this.queue.getCompleted(), this.queue.getCompleted(),
this.queue.getFailed(), this.queue.getFailed(),
this.queue.getDelayed() this.queue.getDelayed(),
]); ]);
return { return {
@ -289,7 +325,7 @@ export class QueueService {
active: active.length, active: active.length,
completed: completed.length, completed: completed.length,
failed: failed.length, failed: failed.length,
delayed: delayed.length delayed: delayed.length,
}; };
} }
async drainQueue() { async drainQueue() {
@ -306,7 +342,7 @@ export class QueueService {
return { return {
...stats, ...stats,
workers: this.workers.length, workers: this.workers.length,
concurrency: this.getTotalConcurrency() concurrency: this.getTotalConcurrency(),
}; };
} }
async shutdown() { async shutdown() {
@ -328,7 +364,7 @@ export class QueueService {
worker.close(), worker.close(),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Worker ${index + 1} close timeout`)), 5000) setTimeout(() => reject(new Error(`Worker ${index + 1} close timeout`)), 5000)
) ),
]); ]);
this.logger.debug(`Worker ${index + 1} closed successfully`); this.logger.debug(`Worker ${index + 1} closed successfully`);
} catch (error) { } catch (error) {
@ -348,15 +384,15 @@ export class QueueService {
this.queue.close(), this.queue.close(),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Queue close timeout')), 3000) setTimeout(() => reject(new Error('Queue close timeout')), 3000)
) ),
]).catch(error => this.logger.error('Queue close error', { error })), ]).catch(error => this.logger.error('Queue close error', { error })),
Promise.race([ Promise.race([
this.queueEvents.close(), this.queueEvents.close(),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('QueueEvents close timeout')), 3000) setTimeout(() => reject(new Error('QueueEvents close timeout')), 3000)
) ),
]).catch(error => this.logger.error('QueueEvents close error', { error })) ]).catch(error => this.logger.error('QueueEvents close error', { error })),
]); ]);
this.logger.info('Queue service shutdown completed successfully'); this.logger.info('Queue service shutdown completed successfully');
@ -367,7 +403,7 @@ export class QueueService {
await Promise.allSettled([ await Promise.allSettled([
...this.workers.map(worker => worker.close(true)), ...this.workers.map(worker => worker.close(true)),
this.queue.close(), this.queue.close(),
this.queueEvents.close() this.queueEvents.close(),
]); ]);
} catch (forceCloseError) { } catch (forceCloseError) {
this.logger.error('Force close also failed', { error: forceCloseError }); this.logger.error('Force close also failed', { error: forceCloseError });

View file

@ -1,5 +1,5 @@
import { CacheProvider, createCache } from '@stock-bot/cache';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { createCache, CacheProvider } from '@stock-bot/cache';
import type { QueueService } from '../services/queue.service'; import type { QueueService } from '../services/queue.service';
const logger = getLogger('batch-helpers'); const logger = getLogger('batch-helpers');
@ -35,7 +35,7 @@ function getCache(): CacheProvider {
cacheProvider = createCache({ cacheProvider = createCache({
keyPrefix: 'batch:', keyPrefix: 'batch:',
ttl: 86400, // 24 hours default ttl: 86400, // 24 hours default
enableMetrics: true enableMetrics: true,
}); });
} }
return cacheProvider; return cacheProvider;
@ -68,7 +68,7 @@ export async function processItems<T>(
jobsCreated: 0, jobsCreated: 0,
mode: 'direct', mode: 'direct',
totalItems: 0, totalItems: 0,
duration: 0 duration: 0,
}; };
} }
@ -76,7 +76,7 @@ export async function processItems<T>(
totalItems: items.length, totalItems: items.length,
mode: options.useBatching ? 'batch' : 'direct', mode: options.useBatching ? 'batch' : 'direct',
batchSize: options.batchSize, batchSize: options.batchSize,
totalDelayHours: options.totalDelayHours totalDelayHours: options.totalDelayHours,
}); });
try { try {
@ -88,11 +88,10 @@ export async function processItems<T>(
logger.info('Batch processing completed', { logger.info('Batch processing completed', {
...result, ...result,
duration: `${(duration / 1000).toFixed(1)}s` duration: `${(duration / 1000).toFixed(1)}s`,
}); });
return { ...result, duration }; return { ...result, duration };
} catch (error) { } catch (error) {
logger.error('Batch processing failed', error); logger.error('Batch processing failed', error);
throw error; throw error;
@ -108,13 +107,12 @@ async function processDirect<T>(
queue: QueueService, queue: QueueService,
options: ProcessOptions options: ProcessOptions
): Promise<Omit<BatchResult, 'duration'>> { ): Promise<Omit<BatchResult, 'duration'>> {
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000;
const delayPerItem = totalDelayMs / items.length; const delayPerItem = totalDelayMs / items.length;
logger.info('Creating direct jobs', { logger.info('Creating direct jobs', {
totalItems: items.length, totalItems: items.length,
delayPerItem: `${(delayPerItem / 1000).toFixed(1)}s` delayPerItem: `${(delayPerItem / 1000).toFixed(1)}s`,
}); });
const jobs = items.map((item, index) => ({ const jobs = items.map((item, index) => ({
@ -124,15 +122,15 @@ async function processDirect<T>(
provider: options.provider || 'generic', provider: options.provider || 'generic',
operation: options.operation || 'process-item', operation: options.operation || 'process-item',
payload: processor(item, index), payload: processor(item, index),
priority: options.priority || 1 priority: options.priority || 1,
}, },
opts: { opts: {
delay: index * delayPerItem, delay: index * delayPerItem,
priority: options.priority || 1, priority: options.priority || 1,
attempts: options.retries || 3, attempts: options.retries || 3,
removeOnComplete: options.removeOnComplete || 10, removeOnComplete: options.removeOnComplete || 10,
removeOnFail: options.removeOnFail || 5 removeOnFail: options.removeOnFail || 5,
} },
})); }));
const createdJobs = await addJobsInChunks(queue, jobs); const createdJobs = await addJobsInChunks(queue, jobs);
@ -140,7 +138,7 @@ async function processDirect<T>(
return { return {
totalItems: items.length, totalItems: items.length,
jobsCreated: createdJobs.length, jobsCreated: createdJobs.length,
mode: 'direct' mode: 'direct',
}; };
} }
@ -153,7 +151,6 @@ async function processBatched<T>(
queue: QueueService, queue: QueueService,
options: ProcessOptions options: ProcessOptions
): Promise<Omit<BatchResult, 'duration'>> { ): Promise<Omit<BatchResult, 'duration'>> {
const batchSize = options.batchSize || 100; const batchSize = options.batchSize || 100;
const batches = createBatches(items, batchSize); const batches = createBatches(items, batchSize);
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000;
@ -163,7 +160,7 @@ async function processBatched<T>(
totalItems: items.length, totalItems: items.length,
batchSize, batchSize,
totalBatches: batches.length, totalBatches: batches.length,
delayPerBatch: `${(delayPerBatch / 1000 / 60).toFixed(2)} minutes` delayPerBatch: `${(delayPerBatch / 1000 / 60).toFixed(2)} minutes`,
}); });
const batchJobs = await Promise.all( const batchJobs = await Promise.all(
@ -180,17 +177,17 @@ async function processBatched<T>(
payloadKey, payloadKey,
batchIndex, batchIndex,
totalBatches: batches.length, totalBatches: batches.length,
itemCount: batch.length itemCount: batch.length,
}, },
priority: options.priority || 2 priority: options.priority || 2,
}, },
opts: { opts: {
delay: batchIndex * delayPerBatch, delay: batchIndex * delayPerBatch,
priority: options.priority || 2, priority: options.priority || 2,
attempts: options.retries || 3, attempts: options.retries || 3,
removeOnComplete: options.removeOnComplete || 10, removeOnComplete: options.removeOnComplete || 10,
removeOnFail: options.removeOnFail || 5 removeOnFail: options.removeOnFail || 5,
} },
}; };
}) })
); );
@ -201,7 +198,7 @@ async function processBatched<T>(
totalItems: items.length, totalItems: items.length,
jobsCreated: createdJobs.length, jobsCreated: createdJobs.length,
batchesCreated: batches.length, batchesCreated: batches.length,
mode: 'batch' mode: 'batch',
}; };
} }
@ -214,7 +211,7 @@ export async function processBatchJob(jobData: any, queue: QueueService): Promis
logger.debug('Processing batch job', { logger.debug('Processing batch job', {
batchIndex, batchIndex,
totalBatches, totalBatches,
itemCount itemCount,
}); });
try { try {
@ -236,13 +233,13 @@ export async function processBatchJob(jobData: any, queue: QueueService): Promis
provider: options.provider || 'generic', provider: options.provider || 'generic',
operation: options.operation || 'generic', operation: options.operation || 'generic',
payload: processor(item, index), payload: processor(item, index),
priority: options.priority || 1 priority: options.priority || 1,
}, },
opts: { opts: {
delay: index * (options.delayPerItem || 1000), delay: index * (options.delayPerItem || 1000),
priority: options.priority || 1, priority: options.priority || 1,
attempts: options.retries || 3 attempts: options.retries || 3,
} },
})); }));
const createdJobs = await addJobsInChunks(queue, jobs); const createdJobs = await addJobsInChunks(queue, jobs);
@ -253,9 +250,8 @@ export async function processBatchJob(jobData: any, queue: QueueService): Promis
return { return {
batchIndex, batchIndex,
itemsProcessed: items.length, itemsProcessed: items.length,
jobsCreated: createdJobs.length jobsCreated: createdJobs.length,
}; };
} catch (error) { } catch (error) {
logger.error('Batch job processing failed', { batchIndex, error }); logger.error('Batch job processing failed', { batchIndex, error });
throw error; throw error;
@ -296,21 +292,21 @@ async function storePayload<T>(
retries: options.retries || 3, retries: options.retries || 3,
// Store routing information for later use // Store routing information for later use
provider: options.provider || 'generic', provider: options.provider || 'generic',
operation: options.operation || 'generic' operation: options.operation || 'generic',
}, },
createdAt: Date.now() createdAt: Date.now(),
}; };
logger.debug('Storing batch payload', { logger.debug('Storing batch payload', {
key, key,
itemCount: items.length itemCount: items.length,
}); });
await cache.set(key, payload, options.ttl || 86400); await cache.set(key, payload, options.ttl || 86400);
logger.debug('Stored batch payload successfully', { logger.debug('Stored batch payload successfully', {
key, key,
itemCount: items.length itemCount: items.length,
}); });
return key; return key;
@ -359,12 +355,10 @@ async function addJobsInChunks(queue: QueueService, jobs: any[], chunkSize = 100
logger.error('Failed to add job chunk', { logger.error('Failed to add job chunk', {
startIndex: i, startIndex: i,
chunkSize: chunk.length, chunkSize: chunk.length,
error error,
}); });
} }
} }
return allCreatedJobs; return allCreatedJobs;
} }

View file

@ -58,7 +58,7 @@ export class MockBroker implements BrokerInterface {
status: 'filled', status: 'filled',
executedPrice: order.price || 100, // Mock price executedPrice: order.price || 100, // Mock price
executedAt: new Date(), executedAt: new Date(),
commission: 1.0 commission: 1.0,
}; };
this.orders.set(orderId, result); this.orders.set(orderId, result);
@ -88,7 +88,7 @@ export class MockBroker implements BrokerInterface {
totalValue: 100000, totalValue: 100000,
availableCash: 50000, availableCash: 50000,
buyingPower: 200000, buyingPower: 200000,
marginUsed: 0 marginUsed: 0,
}; };
} }
} }

View file

@ -1,5 +1,5 @@
import { Order, OrderResult } from '@stock-bot/types';
import { logger } from '@stock-bot/logger'; import { logger } from '@stock-bot/logger';
import { Order, OrderResult } from '@stock-bot/types';
import { BrokerInterface } from '../broker/interface.ts'; import { BrokerInterface } from '../broker/interface.ts';
export class OrderManager { export class OrderManager {
@ -12,7 +12,9 @@ export class OrderManager {
async executeOrder(order: Order): Promise<OrderResult> { async executeOrder(order: Order): Promise<OrderResult> {
try { try {
logger.info(`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`); logger.info(
`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`
);
// Add to pending orders // Add to pending orders
const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@ -26,7 +28,6 @@ export class OrderManager {
logger.info(`Order executed successfully: ${result.orderId}`); logger.info(`Order executed successfully: ${result.orderId}`);
return result; return result;
} catch (error) { } catch (error) {
logger.error('Order execution failed', error); logger.error('Order execution failed', error);
throw error; throw error;

View file

@ -1,5 +1,5 @@
import { Order } from '@stock-bot/types';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { Order } from '@stock-bot/types';
export interface RiskRule { export interface RiskRule {
name: string; name: string;
@ -38,7 +38,7 @@ export class RiskManager {
if (!result.isValid) { if (!result.isValid) {
logger.warn(`Risk rule violation: ${rule.name}`, { logger.warn(`Risk rule violation: ${rule.name}`, {
order, order,
reason: result.reason reason: result.reason,
}); });
return result; return result;
} }
@ -58,12 +58,12 @@ export class RiskManager {
return { return {
isValid: false, isValid: false,
reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`, reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`,
severity: 'error' severity: 'error',
}; };
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} },
}); });
// Balance check rule // Balance check rule
@ -76,12 +76,12 @@ export class RiskManager {
return { return {
isValid: false, isValid: false,
reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`, reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`,
severity: 'error' severity: 'error',
}; };
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} },
}); });
// Concentration risk rule // Concentration risk rule
@ -89,23 +89,25 @@ export class RiskManager {
name: 'ConcentrationLimit', name: 'ConcentrationLimit',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> { async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const currentPosition = context.currentPositions.get(order.symbol) || 0; const currentPosition = context.currentPositions.get(order.symbol) || 0;
const newPosition = order.side === 'buy' ? const newPosition =
currentPosition + order.quantity : order.side === 'buy'
currentPosition - order.quantity; ? currentPosition + order.quantity
: currentPosition - order.quantity;
const positionValue = Math.abs(newPosition) * (order.price || 0); const positionValue = Math.abs(newPosition) * (order.price || 0);
const concentrationRatio = positionValue / context.accountBalance; const concentrationRatio = positionValue / context.accountBalance;
if (concentrationRatio > 0.25) { // 25% max concentration if (concentrationRatio > 0.25) {
// 25% max concentration
return { return {
isValid: false, isValid: false,
reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`, reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`,
severity: 'warning' severity: 'warning',
}; };
} }
return { isValid: true, severity: 'info' }; return { isValid: true, severity: 'info' };
} },
}); });
} }
} }

View file

@ -1,7 +1,8 @@
import { Hono } from 'hono';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { getLogger } from '@stock-bot/logger'; import { Hono } from 'hono';
import { config } from '@stock-bot/config'; import { config } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
// import { BrokerInterface } from './broker/interface.ts'; // import { BrokerInterface } from './broker/interface.ts';
// import { OrderManager } from './execution/order-manager.ts'; // import { OrderManager } from './execution/order-manager.ts';
// import { RiskManager } from './execution/risk-manager.ts'; // import { RiskManager } from './execution/risk-manager.ts';
@ -9,16 +10,16 @@ import { config } from '@stock-bot/config';
const app = new Hono(); const app = new Hono();
const logger = getLogger('execution-service'); const logger = getLogger('execution-service');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', c => {
return c.json({ return c.json({
status: 'healthy', status: 'healthy',
service: 'execution-service', service: 'execution-service',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
}); });
// Order execution endpoints // Order execution endpoints
app.post('/orders/execute', async (c) => { app.post('/orders/execute', async c => {
try { try {
const orderRequest = await c.req.json(); const orderRequest = await c.req.json();
logger.info('Received order execution request', orderRequest); logger.info('Received order execution request', orderRequest);
@ -27,7 +28,7 @@ app.post('/orders/execute', async (c) => {
return c.json({ return c.json({
orderId: `order_${Date.now()}`, orderId: `order_${Date.now()}`,
status: 'pending', status: 'pending',
message: 'Order submitted for execution' message: 'Order submitted for execution',
}); });
} catch (error) { } catch (error) {
logger.error('Order execution failed', error); logger.error('Order execution failed', error);
@ -35,7 +36,7 @@ app.post('/orders/execute', async (c) => {
} }
}); });
app.get('/orders/:orderId/status', async (c) => { app.get('/orders/:orderId/status', async c => {
const orderId = c.req.param('orderId'); const orderId = c.req.param('orderId');
try { try {
@ -43,7 +44,7 @@ app.get('/orders/:orderId/status', async (c) => {
return c.json({ return c.json({
orderId, orderId,
status: 'filled', status: 'filled',
executedAt: new Date().toISOString() executedAt: new Date().toISOString(),
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get order status', error); logger.error('Failed to get order status', error);
@ -51,7 +52,7 @@ app.get('/orders/:orderId/status', async (c) => {
} }
}); });
app.post('/orders/:orderId/cancel', async (c) => { app.post('/orders/:orderId/cancel', async c => {
const orderId = c.req.param('orderId'); const orderId = c.req.param('orderId');
try { try {
@ -59,7 +60,7 @@ app.post('/orders/:orderId/cancel', async (c) => {
return c.json({ return c.json({
orderId, orderId,
status: 'cancelled', status: 'cancelled',
cancelledAt: new Date().toISOString() cancelledAt: new Date().toISOString(),
}); });
} catch (error) { } catch (error) {
logger.error('Failed to cancel order', error); logger.error('Failed to cancel order', error);
@ -68,7 +69,7 @@ app.post('/orders/:orderId/cancel', async (c) => {
}); });
// Risk management endpoints // Risk management endpoints
app.get('/risk/position/:symbol', async (c) => { app.get('/risk/position/:symbol', async c => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
try { try {
@ -77,7 +78,7 @@ app.get('/risk/position/:symbol', async (c) => {
symbol, symbol,
position: 100, position: 100,
exposure: 10000, exposure: 10000,
risk: 'low' risk: 'low',
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get position risk', error); logger.error('Failed to get position risk', error);
@ -89,9 +90,12 @@ const port = config.EXECUTION_SERVICE_PORT || 3004;
logger.info(`Starting execution service on port ${port}`); logger.info(`Starting execution service on port ${port}`);
serve({ serve(
{
fetch: app.fetch, fetch: app.fetch,
port port,
}, (info) => { },
info => {
logger.info(`Execution service is running on port ${info.port}`); logger.info(`Execution service is running on port ${info.port}`);
}); }
);

View file

@ -32,7 +32,9 @@ export class PerformanceAnalyzer {
} }
} }
calculatePerformanceMetrics(period: 'daily' | 'weekly' | 'monthly' = 'daily'): PerformanceMetrics { calculatePerformanceMetrics(
period: 'daily' | 'weekly' | 'monthly' = 'daily'
): PerformanceMetrics {
if (this.snapshots.length < 2) { if (this.snapshots.length < 2) {
throw new Error('Need at least 2 snapshots to calculate performance'); throw new Error('Need at least 2 snapshots to calculate performance');
} }
@ -49,7 +51,7 @@ export class PerformanceAnalyzer {
beta: this.calculateBeta(returns), beta: this.calculateBeta(returns),
alpha: this.calculateAlpha(returns, riskFreeRate), alpha: this.calculateAlpha(returns, riskFreeRate),
calmarRatio: this.calculateCalmarRatio(returns), calmarRatio: this.calculateCalmarRatio(returns),
sortinoRatio: this.calculateSortinoRatio(returns, riskFreeRate) sortinoRatio: this.calculateSortinoRatio(returns, riskFreeRate),
}; };
} }
@ -61,7 +63,7 @@ export class PerformanceAnalyzer {
cvar95: this.calculateCVaR(returns, 0.95), cvar95: this.calculateCVaR(returns, 0.95),
maxDrawdown: this.calculateMaxDrawdown(), maxDrawdown: this.calculateMaxDrawdown(),
downsideDeviation: this.calculateDownsideDeviation(returns), downsideDeviation: this.calculateDownsideDeviation(returns),
correlationMatrix: {} // TODO: Implement correlation matrix correlationMatrix: {}, // TODO: Implement correlation matrix
}; };
} }
@ -100,7 +102,8 @@ export class PerformanceAnalyzer {
if (returns.length === 0) return 0; if (returns.length === 0) return 0;
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; 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; const variance =
returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length;
return Math.sqrt(variance * 252); // Annualized volatility return Math.sqrt(variance * 252); // Annualized volatility
} }
@ -145,7 +148,7 @@ export class PerformanceAnalyzer {
private calculateAlpha(returns: number[], riskFreeRate: number): number { private calculateAlpha(returns: number[], riskFreeRate: number): number {
const beta = this.calculateBeta(returns); const beta = this.calculateBeta(returns);
const portfolioReturn = this.calculateAnnualizedReturn(returns); const portfolioReturn = this.calculateAnnualizedReturn(returns);
const benchmarkReturn = 0.10; // 10% benchmark return (placeholder) const benchmarkReturn = 0.1; // 10% benchmark return (placeholder)
return portfolioReturn - (riskFreeRate + beta * (benchmarkReturn - riskFreeRate)); return portfolioReturn - (riskFreeRate + beta * (benchmarkReturn - riskFreeRate));
} }
@ -174,8 +177,11 @@ export class PerformanceAnalyzer {
const negativeReturns = returns.filter(ret => ret < 0); const negativeReturns = returns.filter(ret => ret < 0);
if (negativeReturns.length === 0) return 0; if (negativeReturns.length === 0) return 0;
const avgNegativeReturn = negativeReturns.reduce((sum, ret) => sum + ret, 0) / negativeReturns.length; const avgNegativeReturn =
const variance = negativeReturns.reduce((sum, ret) => sum + Math.pow(ret - avgNegativeReturn, 2), 0) / negativeReturns.length; 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 return Math.sqrt(variance * 252); // Annualized
} }

View file

@ -1,23 +1,23 @@
import { Hono } from 'hono';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { getLogger } from '@stock-bot/logger'; import { Hono } from 'hono';
import { config } from '@stock-bot/config'; import { config } from '@stock-bot/config';
import { PortfolioManager } from './portfolio/portfolio-manager.ts'; import { getLogger } from '@stock-bot/logger';
import { PerformanceAnalyzer } from './analytics/performance-analyzer.ts'; import { PerformanceAnalyzer } from './analytics/performance-analyzer.ts';
import { PortfolioManager } from './portfolio/portfolio-manager.ts';
const app = new Hono(); const app = new Hono();
const logger = getLogger('portfolio-service'); const logger = getLogger('portfolio-service');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', c => {
return c.json({ return c.json({
status: 'healthy', status: 'healthy',
service: 'portfolio-service', service: 'portfolio-service',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
}); });
// Portfolio endpoints // Portfolio endpoints
app.get('/portfolio/overview', async (c) => { app.get('/portfolio/overview', async c => {
try { try {
// TODO: Get portfolio overview // TODO: Get portfolio overview
return c.json({ return c.json({
@ -26,7 +26,7 @@ app.get('/portfolio/overview', async (c) => {
totalReturnPercent: 25.0, totalReturnPercent: 25.0,
dayChange: 1250, dayChange: 1250,
dayChangePercent: 1.0, dayChangePercent: 1.0,
positions: [] positions: [],
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get portfolio overview', error); logger.error('Failed to get portfolio overview', error);
@ -34,7 +34,7 @@ app.get('/portfolio/overview', async (c) => {
} }
}); });
app.get('/portfolio/positions', async (c) => { app.get('/portfolio/positions', async c => {
try { try {
// TODO: Get current positions // TODO: Get current positions
return c.json([ return c.json([
@ -45,8 +45,8 @@ app.get('/portfolio/positions', async (c) => {
currentPrice: 155.0, currentPrice: 155.0,
marketValue: 15500, marketValue: 15500,
unrealizedPnL: 500, unrealizedPnL: 500,
unrealizedPnLPercent: 3.33 unrealizedPnLPercent: 3.33,
} },
]); ]);
} catch (error) { } catch (error) {
logger.error('Failed to get positions', error); logger.error('Failed to get positions', error);
@ -54,14 +54,14 @@ app.get('/portfolio/positions', async (c) => {
} }
}); });
app.get('/portfolio/history', async (c) => { app.get('/portfolio/history', async c => {
const days = c.req.query('days') || '30'; const days = c.req.query('days') || '30';
try { try {
// TODO: Get portfolio history // TODO: Get portfolio history
return c.json({ return c.json({
period: `${days} days`, period: `${days} days`,
data: [] data: [],
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get portfolio history', error); logger.error('Failed to get portfolio history', error);
@ -70,7 +70,7 @@ app.get('/portfolio/history', async (c) => {
}); });
// Performance analytics endpoints // Performance analytics endpoints
app.get('/analytics/performance', async (c) => { app.get('/analytics/performance', async c => {
const period = c.req.query('period') || '1M'; const period = c.req.query('period') || '1M';
try { try {
@ -78,12 +78,12 @@ app.get('/analytics/performance', async (c) => {
return c.json({ return c.json({
period, period,
totalReturn: 0.25, totalReturn: 0.25,
annualizedReturn: 0.30, annualizedReturn: 0.3,
sharpeRatio: 1.5, sharpeRatio: 1.5,
maxDrawdown: 0.05, maxDrawdown: 0.05,
volatility: 0.15, volatility: 0.15,
beta: 1.1, beta: 1.1,
alpha: 0.02 alpha: 0.02,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get performance analytics', error); logger.error('Failed to get performance analytics', error);
@ -91,7 +91,7 @@ app.get('/analytics/performance', async (c) => {
} }
}); });
app.get('/analytics/risk', async (c) => { app.get('/analytics/risk', async c => {
try { try {
// TODO: Calculate risk metrics // TODO: Calculate risk metrics
return c.json({ return c.json({
@ -99,7 +99,7 @@ app.get('/analytics/risk', async (c) => {
cvar95: 0.03, cvar95: 0.03,
maxDrawdown: 0.05, maxDrawdown: 0.05,
downside_deviation: 0.08, downside_deviation: 0.08,
correlation_matrix: {} correlation_matrix: {},
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get risk analytics', error); logger.error('Failed to get risk analytics', error);
@ -107,13 +107,13 @@ app.get('/analytics/risk', async (c) => {
} }
}); });
app.get('/analytics/attribution', async (c) => { app.get('/analytics/attribution', async c => {
try { try {
// TODO: Calculate performance attribution // TODO: Calculate performance attribution
return c.json({ return c.json({
sector_allocation: {}, sector_allocation: {},
security_selection: {}, security_selection: {},
interaction_effect: {} interaction_effect: {},
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get attribution analytics', error); logger.error('Failed to get attribution analytics', error);
@ -125,9 +125,12 @@ const port = config.PORTFOLIO_SERVICE_PORT || 3005;
logger.info(`Starting portfolio service on port ${port}`); logger.info(`Starting portfolio service on port ${port}`);
serve({ serve(
{
fetch: app.fetch, fetch: app.fetch,
port port,
}, (info) => { },
info => {
logger.info(`Portfolio service is running on port ${info.port}`); logger.info(`Portfolio service is running on port ${info.port}`);
}); }
);

View file

@ -64,9 +64,9 @@ export class PortfolioManager {
unrealizedPnL: 0, unrealizedPnL: 0,
unrealizedPnLPercent: 0, unrealizedPnLPercent: 0,
costBasis: trade.quantity * trade.price + trade.commission, costBasis: trade.quantity * trade.price + trade.commission,
lastUpdated: trade.timestamp lastUpdated: trade.timestamp,
}); });
this.cashBalance -= (trade.quantity * trade.price + trade.commission); this.cashBalance -= trade.quantity * trade.price + trade.commission;
} }
return; return;
} }
@ -74,15 +74,14 @@ export class PortfolioManager {
// Update existing position // Update existing position
if (trade.side === 'buy') { if (trade.side === 'buy') {
const newQuantity = existing.quantity + trade.quantity; const newQuantity = existing.quantity + trade.quantity;
const newCostBasis = existing.costBasis + (trade.quantity * trade.price) + trade.commission; const newCostBasis = existing.costBasis + trade.quantity * trade.price + trade.commission;
existing.quantity = newQuantity; existing.quantity = newQuantity;
existing.averagePrice = (newCostBasis - this.getTotalCommissions(trade.symbol)) / newQuantity; existing.averagePrice = (newCostBasis - this.getTotalCommissions(trade.symbol)) / newQuantity;
existing.costBasis = newCostBasis; existing.costBasis = newCostBasis;
existing.lastUpdated = trade.timestamp; existing.lastUpdated = trade.timestamp;
this.cashBalance -= (trade.quantity * trade.price + trade.commission); this.cashBalance -= trade.quantity * trade.price + trade.commission;
} else if (trade.side === 'sell') { } else if (trade.side === 'sell') {
existing.quantity -= trade.quantity; existing.quantity -= trade.quantity;
existing.lastUpdated = trade.timestamp; existing.lastUpdated = trade.timestamp;
@ -102,8 +101,9 @@ export class PortfolioManager {
if (position) { if (position) {
position.currentPrice = price; position.currentPrice = price;
position.marketValue = position.quantity * price; position.marketValue = position.quantity * price;
position.unrealizedPnL = position.marketValue - (position.quantity * position.averagePrice); position.unrealizedPnL = position.marketValue - position.quantity * position.averagePrice;
position.unrealizedPnLPercent = position.unrealizedPnL / (position.quantity * position.averagePrice) * 100; position.unrealizedPnLPercent =
(position.unrealizedPnL / (position.quantity * position.averagePrice)) * 100;
position.lastUpdated = new Date(); position.lastUpdated = new Date();
} }
} }
@ -130,7 +130,7 @@ export class PortfolioManager {
totalReturn: totalUnrealizedPnL, // Simplified - should include realized gains totalReturn: totalUnrealizedPnL, // Simplified - should include realized gains
totalReturnPercent: (totalUnrealizedPnL / (totalValue - totalUnrealizedPnL)) * 100, totalReturnPercent: (totalUnrealizedPnL / (totalValue - totalUnrealizedPnL)) * 100,
dayChange: 0, // TODO: Calculate from previous day dayChange: 0, // TODO: Calculate from previous day
dayChangePercent: 0 dayChangePercent: 0,
}; };
} }

View file

@ -1,10 +1,10 @@
/** /**
* Processing Service - Technical indicators and data processing * Processing Service - Technical indicators and data processing
*/ */
import { getLogger } from '@stock-bot/logger';
import { loadEnvVariables } from '@stock-bot/config';
import { Hono } from 'hono';
import { serve } from '@hono/node-server'; 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 // Load environment variables
loadEnvVariables(); loadEnvVariables();
@ -14,34 +14,34 @@ const logger = getLogger('processing-service');
const PORT = parseInt(process.env.PROCESSING_SERVICE_PORT || '3003'); const PORT = parseInt(process.env.PROCESSING_SERVICE_PORT || '3003');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', c => {
return c.json({ return c.json({
service: 'processing-service', service: 'processing-service',
status: 'healthy', status: 'healthy',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
}); });
// Technical indicators endpoint // Technical indicators endpoint
app.post('/api/indicators', async (c) => { app.post('/api/indicators', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Technical indicators request', { indicators: body.indicators }); logger.info('Technical indicators request', { indicators: body.indicators });
// TODO: Implement technical indicators processing // TODO: Implement technical indicators processing
return c.json({ return c.json({
message: 'Technical indicators endpoint - not implemented yet', message: 'Technical indicators endpoint - not implemented yet',
requestedIndicators: body.indicators requestedIndicators: body.indicators,
}); });
}); });
// Vectorized processing endpoint // Vectorized processing endpoint
app.post('/api/vectorized/process', async (c) => { app.post('/api/vectorized/process', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Vectorized processing request', { dataPoints: body.data?.length }); logger.info('Vectorized processing request', { dataPoints: body.data?.length });
// TODO: Implement vectorized processing // TODO: Implement vectorized processing
return c.json({ return c.json({
message: 'Vectorized processing endpoint - not implemented yet' message: 'Vectorized processing endpoint - not implemented yet',
}); });
}); });

View file

@ -3,12 +3,7 @@
* Leverages @stock-bot/utils for calculations * Leverages @stock-bot/utils for calculations
*/ */
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { import { ema, macd, rsi, sma } from '@stock-bot/utils';
sma,
ema,
rsi,
macd
} from '@stock-bot/utils';
const logger = getLogger('indicators-service'); const logger = getLogger('indicators-service');
@ -30,7 +25,7 @@ export class IndicatorsService {
logger.info('Calculating indicators', { logger.info('Calculating indicators', {
symbol: request.symbol, symbol: request.symbol,
indicators: request.indicators, indicators: request.indicators,
dataPoints: request.data.length dataPoints: request.data.length,
}); });
const results: Record<string, number[]> = {}; const results: Record<string, number[]> = {};
@ -76,7 +71,7 @@ export class IndicatorsService {
return { return {
symbol: request.symbol, symbol: request.symbol,
timestamp: new Date(), timestamp: new Date(),
indicators: results indicators: results,
}; };
} }
} }

View file

@ -2,7 +2,7 @@
* Event-Driven Backtesting Mode * Event-Driven Backtesting Mode
* Processes data point by point with realistic order execution * Processes data point by point with realistic order execution
*/ */
import { ExecutionMode, Order, OrderResult, MarketData } from '../../framework/execution-mode'; import { ExecutionMode, MarketData, Order, OrderResult } from '../../framework/execution-mode';
export interface BacktestConfig { export interface BacktestConfig {
startDate: Date; startDate: Date;
@ -25,7 +25,7 @@ export class EventMode extends ExecutionMode {
async executeOrder(order: Order): Promise<OrderResult> { async executeOrder(order: Order): Promise<OrderResult> {
this.logger.debug('Simulating order execution', { this.logger.debug('Simulating order execution', {
orderId: order.id, orderId: order.id,
simulationTime: this.simulationTime simulationTime: this.simulationTime,
}); });
// TODO: Implement realistic order simulation // TODO: Implement realistic order simulation
@ -38,7 +38,7 @@ export class EventMode extends ExecutionMode {
commission: 1.0, // TODO: Calculate based on commission model commission: 1.0, // TODO: Calculate based on commission model
slippage: 0.01, // TODO: Calculate based on slippage model slippage: 0.01, // TODO: Calculate based on slippage model
timestamp: this.simulationTime, timestamp: this.simulationTime,
executionTime: 50 // ms executionTime: 50, // ms
}; };
return simulatedResult; return simulatedResult;

View file

@ -1,11 +1,11 @@
import { getLogger } from '@stock-bot/logger'; import { create } from 'domain';
import { EventBus } from '@stock-bot/event-bus';
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
import { DataFrame } from '@stock-bot/data-frame'; import { DataFrame } from '@stock-bot/data-frame';
import { ExecutionMode, BacktestContext, BacktestResult } from '../framework/execution-mode'; 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 { EventMode } from './event-mode';
import VectorizedMode from './vectorized-mode'; import VectorizedMode from './vectorized-mode';
import { create } from 'domain';
export interface HybridModeConfig { export interface HybridModeConfig {
vectorizedThreshold: number; // Switch to vectorized if data points > threshold vectorizedThreshold: number; // Switch to vectorized if data points > threshold
@ -23,11 +23,7 @@ export class HybridMode extends ExecutionMode {
private precomputedIndicators: Map<string, number[]> = new Map(); private precomputedIndicators: Map<string, number[]> = new Map();
private currentIndex: number = 0; private currentIndex: number = 0;
constructor( constructor(context: BacktestContext, eventBus: EventBus, config: HybridModeConfig = {}) {
context: BacktestContext,
eventBus: EventBus,
config: HybridModeConfig = {}
) {
super(context, eventBus); super(context, eventBus);
this.config = { this.config = {
@ -36,7 +32,7 @@ export class HybridMode extends ExecutionMode {
eventDrivenRealtime: true, eventDrivenRealtime: true,
optimizeIndicators: true, optimizeIndicators: true,
batchSize: 10000, batchSize: 10000,
...config ...config,
}; };
this.vectorEngine = new VectorEngine(); this.vectorEngine = new VectorEngine();
@ -55,7 +51,7 @@ export class HybridMode extends ExecutionMode {
this.logger.info('Hybrid mode initialized', { this.logger.info('Hybrid mode initialized', {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
config: this.config config: this.config,
}); });
} }
@ -76,18 +72,16 @@ export class HybridMode extends ExecutionMode {
// Large dataset: use hybrid approach // Large dataset: use hybrid approach
this.logger.info('Using hybrid approach for large dataset', { dataSize }); this.logger.info('Using hybrid approach for large dataset', { dataSize });
return await this.executeHybrid(startTime); return await this.executeHybrid(startTime);
} catch (error) { } catch (error) {
this.logger.error('Hybrid backtest failed', { this.logger.error('Hybrid backtest failed', {
error, error,
backtestId: this.context.backtestId backtestId: this.context.backtestId,
}); });
await this.eventBus.publishBacktestUpdate( await this.eventBus.publishBacktestUpdate(this.context.backtestId, 0, {
this.context.backtestId, status: 'failed',
0, error: error.message,
{ status: 'failed', error: error.message } });
);
throw error; throw error;
} }
@ -103,18 +97,17 @@ export class HybridMode extends ExecutionMode {
// Phase 3: Combine results // Phase 3: Combine results
const combinedResult = this.combineResults(warmupResult, eventResult, startTime); const combinedResult = this.combineResults(warmupResult, eventResult, startTime);
await this.eventBus.publishBacktestUpdate( await this.eventBus.publishBacktestUpdate(this.context.backtestId, 100, {
this.context.backtestId, status: 'completed',
100, result: combinedResult,
{ status: 'completed', result: combinedResult } });
);
this.logger.info('Hybrid backtest completed', { this.logger.info('Hybrid backtest completed', {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
duration: Date.now() - startTime, duration: Date.now() - startTime,
totalTrades: combinedResult.trades.length, totalTrades: combinedResult.trades.length,
warmupTrades: warmupResult.trades.length, warmupTrades: warmupResult.trades.length,
eventTrades: eventResult.trades.length eventTrades: eventResult.trades.length,
}); });
return combinedResult; return combinedResult;
@ -122,7 +115,7 @@ export class HybridMode extends ExecutionMode {
private async executeWarmupPhase(): Promise<BacktestResult> { private async executeWarmupPhase(): Promise<BacktestResult> {
this.logger.info('Executing vectorized warmup phase', { this.logger.info('Executing vectorized warmup phase', {
warmupPeriod: this.config.warmupPeriod warmupPeriod: this.config.warmupPeriod,
}); });
// Load warmup data // Load warmup data
@ -154,7 +147,7 @@ export class HybridMode extends ExecutionMode {
// Create modified context for event phase // Create modified context for event phase
const eventContext: BacktestContext = { const eventContext: BacktestContext = {
...this.context, ...this.context,
initialPortfolio: this.extractFinalPortfolio(warmupResult) initialPortfolio: this.extractFinalPortfolio(warmupResult),
}; };
// Execute event-driven backtest for remaining data // Execute event-driven backtest for remaining data
@ -198,7 +191,7 @@ export class HybridMode extends ExecutionMode {
this.precomputedIndicators.set('bb_lower', bb.lower); this.precomputedIndicators.set('bb_lower', bb.lower);
this.logger.info('Indicators pre-computed', { this.logger.info('Indicators pre-computed', {
indicators: Array.from(this.precomputedIndicators.keys()) indicators: Array.from(this.precomputedIndicators.keys()),
}); });
} }
@ -232,7 +225,7 @@ export class HybridMode extends ExecutionMode {
this.logger.debug('Estimated data size', { this.logger.debug('Estimated data size', {
timeRange, timeRange,
estimatedPoints, estimatedPoints,
threshold: this.config.vectorizedThreshold threshold: this.config.vectorizedThreshold,
}); });
return estimatedPoints; return estimatedPoints;
@ -243,10 +236,10 @@ export class HybridMode extends ExecutionMode {
// This should load more data than just the warmup period for indicator calculations // This should load more data than just the warmup period for indicator calculations
const data = []; const data = [];
const startTime = new Date(this.context.startDate).getTime(); const startTime = new Date(this.context.startDate).getTime();
const warmupEndTime = startTime + (this.config.warmupPeriod * 60000); const warmupEndTime = startTime + this.config.warmupPeriod * 60000;
// Add extra lookback for indicator calculations // Add extra lookback for indicator calculations
const lookbackTime = startTime - (200 * 60000); // 200 periods lookback const lookbackTime = startTime - 200 * 60000; // 200 periods lookback
for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) { for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) {
const basePrice = 100 + Math.sin(timestamp / 1000000) * 10; const basePrice = 100 + Math.sin(timestamp / 1000000) * 10;
@ -265,7 +258,7 @@ export class HybridMode extends ExecutionMode {
high, high,
low, low,
close, close,
volume volume,
}); });
} }
@ -282,8 +275,8 @@ export class HybridMode extends ExecutionMode {
high: 'number', high: 'number',
low: 'number', low: 'number',
close: 'number', close: 'number',
volume: 'number' volume: 'number',
} },
}); });
} }
@ -298,7 +291,10 @@ export class HybridMode extends ExecutionMode {
return strategy.code || 'sma_crossover'; return strategy.code || 'sma_crossover';
} }
private convertVectorizedResult(vectorResult: VectorizedBacktestResult, startTime: number): BacktestResult { private convertVectorizedResult(
vectorResult: VectorizedBacktestResult,
startTime: number
): BacktestResult {
return { return {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
strategy: this.context.strategy, strategy: this.context.strategy,
@ -318,7 +314,7 @@ export class HybridMode extends ExecutionMode {
quantity: trade.quantity, quantity: trade.quantity,
pnl: trade.pnl, pnl: trade.pnl,
commission: 0, commission: 0,
slippage: 0 slippage: 0,
})), })),
performance: { performance: {
totalReturn: vectorResult.metrics.totalReturns, totalReturn: vectorResult.metrics.totalReturns,
@ -330,12 +326,14 @@ export class HybridMode extends ExecutionMode {
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length, winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length, losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
avgTrade: vectorResult.metrics.avgTrade, avgTrade: vectorResult.metrics.avgTrade,
avgWin: vectorResult.trades.filter(t => t.pnl > 0) avgWin:
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0, vectorResult.trades.filter(t => t.pnl > 0).reduce((sum, t) => sum + t.pnl, 0) /
avgLoss: vectorResult.trades.filter(t => t.pnl <= 0) vectorResult.trades.filter(t => t.pnl > 0).length || 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), largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0) largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0),
}, },
equity: vectorResult.equity, equity: vectorResult.equity,
drawdown: vectorResult.metrics.drawdown, drawdown: vectorResult.metrics.drawdown,
@ -343,8 +341,8 @@ export class HybridMode extends ExecutionMode {
mode: 'hybrid-vectorized', mode: 'hybrid-vectorized',
dataPoints: vectorResult.timestamps.length, dataPoints: vectorResult.timestamps.length,
signals: Object.keys(vectorResult.signals), signals: Object.keys(vectorResult.signals),
optimizations: ['vectorized_warmup', 'precomputed_indicators'] optimizations: ['vectorized_warmup', 'precomputed_indicators'],
} },
}; };
} }
@ -355,11 +353,15 @@ export class HybridMode extends ExecutionMode {
return { return {
cash: finalEquity, cash: finalEquity,
positions: [], // Simplified - in production would track actual positions positions: [], // Simplified - in production would track actual positions
equity: finalEquity equity: finalEquity,
}; };
} }
private combineResults(warmupResult: BacktestResult, eventResult: BacktestResult, startTime: number): BacktestResult { private combineResults(
warmupResult: BacktestResult,
eventResult: BacktestResult,
startTime: number
): BacktestResult {
// Combine results from both phases // Combine results from both phases
const combinedTrades = [...warmupResult.trades, ...eventResult.trades]; const combinedTrades = [...warmupResult.trades, ...eventResult.trades];
const combinedEquity = [...warmupResult.equity, ...eventResult.equity]; const combinedEquity = [...warmupResult.equity, ...eventResult.equity];
@ -383,7 +385,8 @@ export class HybridMode extends ExecutionMode {
duration: Date.now() - startTime, duration: Date.now() - startTime,
trades: combinedTrades, trades: combinedTrades,
performance: { performance: {
totalReturn: (combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0], totalReturn:
(combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0],
sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation
maxDrawdown: Math.max(...combinedDrawdown), maxDrawdown: Math.max(...combinedDrawdown),
winRate: winningTrades.length / combinedTrades.length, winRate: winningTrades.length / combinedTrades.length,
@ -395,7 +398,7 @@ export class HybridMode extends ExecutionMode {
avgWin: grossProfit / winningTrades.length || 0, avgWin: grossProfit / winningTrades.length || 0,
avgLoss: grossLoss / losingTrades.length || 0, avgLoss: grossLoss / losingTrades.length || 0,
largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0), largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0),
largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0) largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0),
}, },
equity: combinedEquity, equity: combinedEquity,
drawdown: combinedDrawdown, drawdown: combinedDrawdown,
@ -405,8 +408,8 @@ export class HybridMode extends ExecutionMode {
warmupPeriod: this.config.warmupPeriod, warmupPeriod: this.config.warmupPeriod,
optimizations: ['precomputed_indicators', 'hybrid_execution'], optimizations: ['precomputed_indicators', 'hybrid_execution'],
warmupTrades: warmupResult.trades.length, warmupTrades: warmupResult.trades.length,
eventTrades: eventResult.trades.length eventTrades: eventResult.trades.length,
} },
}; };
} }

View file

@ -2,7 +2,7 @@
* Live Trading Mode * Live Trading Mode
* Executes orders through real brokers * Executes orders through real brokers
*/ */
import { ExecutionMode, Order, OrderResult, MarketData } from '../../framework/execution-mode'; import { ExecutionMode, MarketData, Order, OrderResult } from '../../framework/execution-mode';
export class LiveMode extends ExecutionMode { export class LiveMode extends ExecutionMode {
name = 'live'; name = 'live';

View file

@ -1,8 +1,8 @@
import { getLogger } from '@stock-bot/logger';
import { EventBus } from '@stock-bot/event-bus';
import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine';
import { DataFrame } from '@stock-bot/data-frame'; import { DataFrame } from '@stock-bot/data-frame';
import { ExecutionMode, BacktestContext, BacktestResult } from '../framework/execution-mode'; 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 { export interface VectorizedModeConfig {
batchSize?: number; batchSize?: number;
@ -15,18 +15,14 @@ export class VectorizedMode extends ExecutionMode {
private config: VectorizedModeConfig; private config: VectorizedModeConfig;
private logger = getLogger('vectorized-mode'); private logger = getLogger('vectorized-mode');
constructor( constructor(context: BacktestContext, eventBus: EventBus, config: VectorizedModeConfig = {}) {
context: BacktestContext,
eventBus: EventBus,
config: VectorizedModeConfig = {}
) {
super(context, eventBus); super(context, eventBus);
this.vectorEngine = new VectorEngine(); this.vectorEngine = new VectorEngine();
this.config = { this.config = {
batchSize: 10000, batchSize: 10000,
enableOptimization: true, enableOptimization: true,
parallelProcessing: true, parallelProcessing: true,
...config ...config,
}; };
} }
@ -34,7 +30,7 @@ export class VectorizedMode extends ExecutionMode {
await super.initialize(); await super.initialize();
this.logger.info('Vectorized mode initialized', { this.logger.info('Vectorized mode initialized', {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
config: this.config config: this.config,
}); });
} }
@ -60,31 +56,28 @@ export class VectorizedMode extends ExecutionMode {
const result = this.convertVectorizedResult(vectorResult, startTime); const result = this.convertVectorizedResult(vectorResult, startTime);
// Emit completion event // Emit completion event
await this.eventBus.publishBacktestUpdate( await this.eventBus.publishBacktestUpdate(this.context.backtestId, 100, {
this.context.backtestId, status: 'completed',
100, result,
{ status: 'completed', result } });
);
this.logger.info('Vectorized backtest completed', { this.logger.info('Vectorized backtest completed', {
backtestId: this.context.backtestId, backtestId: this.context.backtestId,
duration: Date.now() - startTime, duration: Date.now() - startTime,
totalTrades: result.trades.length totalTrades: result.trades.length,
}); });
return result; return result;
} catch (error) { } catch (error) {
this.logger.error('Vectorized backtest failed', { this.logger.error('Vectorized backtest failed', {
error, error,
backtestId: this.context.backtestId backtestId: this.context.backtestId,
}); });
await this.eventBus.publishBacktestUpdate( await this.eventBus.publishBacktestUpdate(this.context.backtestId, 0, {
this.context.backtestId, status: 'failed',
0, error: error.message,
{ status: 'failed', error: error.message } });
);
throw error; throw error;
} }
@ -118,7 +111,7 @@ export class VectorizedMode extends ExecutionMode {
high, high,
low, low,
close, close,
volume volume,
}); });
} }
@ -135,8 +128,8 @@ export class VectorizedMode extends ExecutionMode {
high: 'number', high: 'number',
low: 'number', low: 'number',
close: 'number', close: 'number',
volume: 'number' volume: 'number',
} },
}); });
} }
@ -176,7 +169,7 @@ export class VectorizedMode extends ExecutionMode {
quantity: trade.quantity, quantity: trade.quantity,
pnl: trade.pnl, pnl: trade.pnl,
commission: 0, // Simplified commission: 0, // Simplified
slippage: 0 slippage: 0,
})), })),
performance: { performance: {
totalReturn: vectorResult.metrics.totalReturns, totalReturn: vectorResult.metrics.totalReturns,
@ -188,12 +181,14 @@ export class VectorizedMode extends ExecutionMode {
winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length, winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length,
losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length, losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length,
avgTrade: vectorResult.metrics.avgTrade, avgTrade: vectorResult.metrics.avgTrade,
avgWin: vectorResult.trades.filter(t => t.pnl > 0) avgWin:
.reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0, vectorResult.trades.filter(t => t.pnl > 0).reduce((sum, t) => sum + t.pnl, 0) /
avgLoss: vectorResult.trades.filter(t => t.pnl <= 0) vectorResult.trades.filter(t => t.pnl > 0).length || 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), largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0),
largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0) largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0),
}, },
equity: vectorResult.equity, equity: vectorResult.equity,
drawdown: vectorResult.metrics.drawdown, drawdown: vectorResult.metrics.drawdown,
@ -201,8 +196,8 @@ export class VectorizedMode extends ExecutionMode {
mode: 'vectorized', mode: 'vectorized',
dataPoints: vectorResult.timestamps.length, dataPoints: vectorResult.timestamps.length,
signals: Object.keys(vectorResult.signals), signals: Object.keys(vectorResult.signals),
optimizations: this.config.enableOptimization ? ['vectorized_computation'] : [] optimizations: this.config.enableOptimization ? ['vectorized_computation'] : [],
} },
}; };
} }
@ -212,9 +207,11 @@ export class VectorizedMode extends ExecutionMode {
} }
// Batch processing capabilities // Batch processing capabilities
async batchBacktest(strategies: Array<{ id: string; config: any }>): Promise<Record<string, BacktestResult>> { async batchBacktest(
strategies: Array<{ id: string; config: any }>
): Promise<Record<string, BacktestResult>> {
this.logger.info('Starting batch vectorized backtest', { this.logger.info('Starting batch vectorized backtest', {
strategiesCount: strategies.length strategiesCount: strategies.length,
}); });
const data = await this.loadHistoricalData(); const data = await this.loadHistoricalData();
@ -222,7 +219,7 @@ export class VectorizedMode extends ExecutionMode {
const strategyConfigs = strategies.map(s => ({ const strategyConfigs = strategies.map(s => ({
id: s.id, id: s.id,
code: this.generateStrategyCode() code: this.generateStrategyCode(),
})); }));
const batchResults = await this.vectorEngine.batchBacktest(dataFrame, strategyConfigs); const batchResults = await this.vectorEngine.batchBacktest(dataFrame, strategyConfigs);

View file

@ -3,15 +3,14 @@
* Strategy Service CLI * Strategy Service CLI
* Command-line interface for running backtests and managing strategies * Command-line interface for running backtests and managing strategies
*/ */
import { program } from 'commander'; import { program } from 'commander';
import { getLogger } from '@stock-bot/logger';
import { createEventBus } from '@stock-bot/event-bus'; import { createEventBus } from '@stock-bot/event-bus';
import { BacktestContext } from '../framework/execution-mode'; import { getLogger } from '@stock-bot/logger';
import { LiveMode } from '../backtesting/modes/live-mode';
import { EventMode } from '../backtesting/modes/event-mode'; import { EventMode } from '../backtesting/modes/event-mode';
import VectorizedMode from '../backtesting/modes/vectorized-mode';
import HybridMode from '../backtesting/modes/hybrid-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'); const logger = getLogger('strategy-cli');
@ -35,7 +34,7 @@ async function runBacktest(options: CLIBacktestConfig): Promise<void> {
// Initialize event bus // Initialize event bus
const eventBus = createEventBus({ const eventBus = createEventBus({
serviceName: 'strategy-cli', serviceName: 'strategy-cli',
enablePersistence: false // Disable Redis for CLI enablePersistence: false, // Disable Redis for CLI
}); });
// Create backtest context // Create backtest context
@ -46,13 +45,13 @@ async function runBacktest(options: CLIBacktestConfig): Promise<void> {
name: options.strategy, name: options.strategy,
type: options.strategy, type: options.strategy,
code: options.strategy, code: options.strategy,
parameters: {} parameters: {},
}, },
symbol: options.symbol, symbol: options.symbol,
startDate: options.startDate, startDate: options.startDate,
endDate: options.endDate, endDate: options.endDate,
initialCapital: options.initialCapital || 10000, initialCapital: options.initialCapital || 10000,
mode: options.mode mode: options.mode,
}; };
// Load additional config if provided // Load additional config if provided
@ -82,7 +81,7 @@ async function runBacktest(options: CLIBacktestConfig): Promise<void> {
} }
// Subscribe to progress updates // Subscribe to progress updates
eventBus.subscribe('backtest.update', (message) => { eventBus.subscribe('backtest.update', message => {
const { backtestId, progress, ...data } = message.data; const { backtestId, progress, ...data } = message.data;
console.log(`Progress: ${progress}%`, data); console.log(`Progress: ${progress}%`, data);
}); });
@ -100,7 +99,6 @@ async function runBacktest(options: CLIBacktestConfig): Promise<void> {
} }
await eventBus.close(); await eventBus.close();
} catch (error) { } catch (error) {
logger.error('Backtest failed', error); logger.error('Backtest failed', error);
process.exit(1); process.exit(1);
@ -178,9 +176,9 @@ function convertTradesToCSV(trades: any[]): string {
const headers = Object.keys(trades[0]).join(','); const headers = Object.keys(trades[0]).join(',');
const rows = trades.map(trade => const rows = trades.map(trade =>
Object.values(trade).map(value => Object.values(trade)
typeof value === 'string' ? `"${value}"` : value .map(value => (typeof value === 'string' ? `"${value}"` : value))
).join(',') .join(',')
); );
return [headers, ...rows].join('\n'); return [headers, ...rows].join('\n');
@ -202,7 +200,13 @@ async function validateStrategy(strategy: string): Promise<void> {
// TODO: Add strategy validation logic // TODO: Add strategy validation logic
// This could check if the strategy exists, has valid parameters, etc. // This could check if the strategy exists, has valid parameters, etc.
const validStrategies = ['sma_crossover', 'ema_crossover', 'rsi_mean_reversion', 'macd_trend', 'bollinger_bands']; const validStrategies = [
'sma_crossover',
'ema_crossover',
'rsi_mean_reversion',
'macd_trend',
'bollinger_bands',
];
if (!validStrategies.includes(strategy)) { if (!validStrategies.includes(strategy)) {
console.warn(`Warning: Strategy '${strategy}' is not in the list of known strategies`); console.warn(`Warning: Strategy '${strategy}' is not in the list of known strategies`);
@ -213,10 +217,7 @@ async function validateStrategy(strategy: string): Promise<void> {
} }
// CLI Commands // CLI Commands
program program.name('strategy-cli').description('Stock Trading Bot Strategy CLI').version('1.0.0');
.name('strategy-cli')
.description('Stock Trading Bot Strategy CLI')
.version('1.0.0');
program program
.command('backtest') .command('backtest')
@ -234,10 +235,7 @@ program
await runBacktest(options); await runBacktest(options);
}); });
program program.command('list-strategies').description('List available strategies').action(listStrategies);
.command('list-strategies')
.description('List available strategies')
.action(listStrategies);
program program
.command('validate') .command('validate')
@ -269,7 +267,7 @@ program
await runBacktest({ await runBacktest({
...options, ...options,
strategy, strategy,
output: options.output ? `${options.output}/${strategy}.json` : undefined output: options.output ? `${options.output}/${strategy}.json` : undefined,
}); });
} catch (error) { } catch (error) {
console.error(`Failed to run ${strategy}:`, (error as Error).message); console.error(`Failed to run ${strategy}:`, (error as Error).message);

View file

@ -51,7 +51,7 @@ export enum BacktestMode {
LIVE = 'live', LIVE = 'live',
EVENT_DRIVEN = 'event-driven', EVENT_DRIVEN = 'event-driven',
VECTORIZED = 'vectorized', VECTORIZED = 'vectorized',
HYBRID = 'hybrid' HYBRID = 'hybrid',
} }
export class ModeFactory { export class ModeFactory {

View file

@ -1,10 +1,10 @@
/** /**
* Strategy Service - Multi-mode strategy execution and backtesting * Strategy Service - Multi-mode strategy execution and backtesting
*/ */
import { getLogger } from '@stock-bot/logger';
import { loadEnvVariables } from '@stock-bot/config';
import { Hono } from 'hono';
import { serve } from '@hono/node-server'; 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 // Load environment variables
loadEnvVariables(); loadEnvVariables();
@ -14,69 +14,69 @@ const logger = getLogger('strategy-service');
const PORT = parseInt(process.env.STRATEGY_SERVICE_PORT || '3004'); const PORT = parseInt(process.env.STRATEGY_SERVICE_PORT || '3004');
// Health check endpoint // Health check endpoint
app.get('/health', (c) => { app.get('/health', c => {
return c.json({ return c.json({
service: 'strategy-service', service: 'strategy-service',
status: 'healthy', status: 'healthy',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
}); });
// Strategy execution endpoints // Strategy execution endpoints
app.post('/api/strategy/run', async (c) => { app.post('/api/strategy/run', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Strategy run request', { logger.info('Strategy run request', {
strategy: body.strategy, strategy: body.strategy,
mode: body.mode mode: body.mode,
}); });
// TODO: Implement strategy execution // TODO: Implement strategy execution
return c.json({ return c.json({
message: 'Strategy execution endpoint - not implemented yet', message: 'Strategy execution endpoint - not implemented yet',
strategy: body.strategy, strategy: body.strategy,
mode: body.mode mode: body.mode,
}); });
}); });
// Backtesting endpoints // Backtesting endpoints
app.post('/api/backtest/event', async (c) => { app.post('/api/backtest/event', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Event-driven backtest request', { strategy: body.strategy }); logger.info('Event-driven backtest request', { strategy: body.strategy });
// TODO: Implement event-driven backtesting // TODO: Implement event-driven backtesting
return c.json({ return c.json({
message: 'Event-driven backtest endpoint - not implemented yet' message: 'Event-driven backtest endpoint - not implemented yet',
}); });
}); });
app.post('/api/backtest/vector', async (c) => { app.post('/api/backtest/vector', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Vectorized backtest request', { strategy: body.strategy }); logger.info('Vectorized backtest request', { strategy: body.strategy });
// TODO: Implement vectorized backtesting // TODO: Implement vectorized backtesting
return c.json({ return c.json({
message: 'Vectorized backtest endpoint - not implemented yet' message: 'Vectorized backtest endpoint - not implemented yet',
}); });
}); });
app.post('/api/backtest/hybrid', async (c) => { app.post('/api/backtest/hybrid', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Hybrid backtest request', { strategy: body.strategy }); logger.info('Hybrid backtest request', { strategy: body.strategy });
// TODO: Implement hybrid backtesting // TODO: Implement hybrid backtesting
return c.json({ return c.json({
message: 'Hybrid backtest endpoint - not implemented yet' message: 'Hybrid backtest endpoint - not implemented yet',
}); });
}); });
// Parameter optimization endpoint // Parameter optimization endpoint
app.post('/api/optimize', async (c) => { app.post('/api/optimize', async c => {
const body = await c.req.json(); const body = await c.req.json();
logger.info('Parameter optimization request', { strategy: body.strategy }); logger.info('Parameter optimization request', { strategy: body.strategy });
// TODO: Implement parameter optimization // TODO: Implement parameter optimization
return c.json({ return c.json({
message: 'Parameter optimization endpoint - not implemented yet' message: 'Parameter optimization endpoint - not implemented yet',
}); });
}); });

View file

@ -8,6 +8,7 @@
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
}, },
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
"@testcontainers/mongodb": "^10.7.2", "@testcontainers/mongodb": "^10.7.2",
"@testcontainers/postgresql": "^10.7.2", "@testcontainers/postgresql": "^10.7.2",
"@types/bun": "latest", "@types/bun": "latest",
@ -17,6 +18,7 @@
"bun-types": "^1.2.15", "bun-types": "^1.2.15",
"mongodb-memory-server": "^9.1.6", "mongodb-memory-server": "^9.1.6",
"pg-mem": "^2.8.1", "pg-mem": "^2.8.1",
"prettier": "^3.5.3",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"turbo": "^2.5.4", "turbo": "^2.5.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
@ -533,6 +535,8 @@
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
"@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.4.2", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@vue/compiler-sfc": "2.7.x || 3.x", "prettier": "2 || 3 || ^4.0.0-0" }, "optionalPeers": ["@vue/compiler-sfc"] }, "sha512-KkVFy3TLh0OFzimbZglMmORi+vL/i2OFhEs5M07R9w0IwWAGpsNNyE4CY/2u0YoMF5bawKC2+8/fUH60nnNtjw=="],
"@inquirer/checkbox": ["@inquirer/checkbox@4.1.8", "", { "dependencies": { "@inquirer/core": "^10.1.13", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg=="], "@inquirer/checkbox": ["@inquirer/checkbox@4.1.8", "", { "dependencies": { "@inquirer/core": "^10.1.13", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg=="],
"@inquirer/confirm": ["@inquirer/confirm@5.1.10", "", { "dependencies": { "@inquirer/core": "^10.1.11", "@inquirer/type": "^3.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g=="], "@inquirer/confirm": ["@inquirer/confirm@5.1.10", "", { "dependencies": { "@inquirer/core": "^10.1.11", "@inquirer/type": "^3.0.6" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g=="],
@ -1775,6 +1779,8 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],

View file

@ -1,6 +1,6 @@
import Redis from 'ioredis'; import Redis from 'ioredis';
import { getLogger } from '@stock-bot/logger';
import { dragonflyConfig } from '@stock-bot/config'; import { dragonflyConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
interface ConnectionConfig { interface ConnectionConfig {
name: string; name: string;
@ -66,7 +66,9 @@ export class RedisConnectionManager {
retryDelayOnFailover: dragonflyConfig.DRAGONFLY_RETRY_DELAY, retryDelayOnFailover: dragonflyConfig.DRAGONFLY_RETRY_DELAY,
connectTimeout: dragonflyConfig.DRAGONFLY_CONNECT_TIMEOUT, connectTimeout: dragonflyConfig.DRAGONFLY_CONNECT_TIMEOUT,
commandTimeout: dragonflyConfig.DRAGONFLY_COMMAND_TIMEOUT, commandTimeout: dragonflyConfig.DRAGONFLY_COMMAND_TIMEOUT,
keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE ? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000 : 0, keepAlive: dragonflyConfig.DRAGONFLY_ENABLE_KEEPALIVE
? dragonflyConfig.DRAGONFLY_KEEPALIVE_INTERVAL * 1000
: 0,
connectionName: name, connectionName: name,
lazyConnect: false, // Connect immediately instead of waiting for first command lazyConnect: false, // Connect immediately instead of waiting for first command
...(dragonflyConfig.DRAGONFLY_TLS && { ...(dragonflyConfig.DRAGONFLY_TLS && {
@ -90,7 +92,7 @@ export class RedisConnectionManager {
this.logger.info(`Redis connection ready: ${name}`); this.logger.info(`Redis connection ready: ${name}`);
}); });
redis.on('error', (err) => { redis.on('error', err => {
this.logger.error(`Redis connection error for ${name}:`, err); this.logger.error(`Redis connection error for ${name}:`, err);
}); });
@ -129,8 +131,8 @@ export class RedisConnectionManager {
// Close shared connections (only if this is the last instance) // Close shared connections (only if this is the last instance)
if (RedisConnectionManager.instance === this) { if (RedisConnectionManager.instance === this) {
const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map(conn => const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map(
this.closeConnection(conn) conn => this.closeConnection(conn)
); );
await Promise.all(sharedPromises); await Promise.all(sharedPromises);
RedisConnectionManager.sharedConnections.clear(); RedisConnectionManager.sharedConnections.clear();
@ -145,7 +147,7 @@ export class RedisConnectionManager {
getConnectionCount(): { shared: number; unique: number } { getConnectionCount(): { shared: number; unique: number } {
return { return {
shared: RedisConnectionManager.sharedConnections.size, shared: RedisConnectionManager.sharedConnections.size,
unique: this.connections.size unique: this.connections.size,
}; };
} }
@ -155,7 +157,7 @@ export class RedisConnectionManager {
getConnectionNames(): { shared: string[]; unique: string[] } { getConnectionNames(): { shared: string[]; unique: string[] } {
return { return {
shared: Array.from(RedisConnectionManager.sharedConnections.keys()), shared: Array.from(RedisConnectionManager.sharedConnections.keys()),
unique: Array.from(this.connections.keys()) unique: Array.from(this.connections.keys()),
}; };
} }
@ -198,10 +200,7 @@ export class RedisConnectionManager {
*/ */
static async waitForAllConnections(timeout: number = 30000): Promise<void> { static async waitForAllConnections(timeout: number = 30000): Promise<void> {
const instance = this.getInstance(); const instance = this.getInstance();
const allConnections = new Map([ const allConnections = new Map([...instance.connections, ...this.sharedConnections]);
...instance.connections,
...this.sharedConnections
]);
if (allConnections.size === 0) { if (allConnections.size === 0) {
instance.logger.info('No Redis connections to wait for'); instance.logger.info('No Redis connections to wait for');
@ -259,14 +258,11 @@ export class RedisConnectionManager {
*/ */
static areAllConnectionsReady(): boolean { static areAllConnectionsReady(): boolean {
const instance = this.getInstance(); const instance = this.getInstance();
const allConnections = new Map([ const allConnections = new Map([...instance.connections, ...this.sharedConnections]);
...instance.connections,
...this.sharedConnections
]);
return allConnections.size > 0 && return (
Array.from(allConnections.keys()).every(name => allConnections.size > 0 &&
this.readyConnections.has(name) Array.from(allConnections.keys()).every(name => this.readyConnections.has(name))
); );
} }
} }

View file

@ -1,6 +1,6 @@
import { RedisCache } from './redis-cache';
import { RedisConnectionManager } from './connection-manager'; import { RedisConnectionManager } from './connection-manager';
import type { CacheProvider, CacheOptions } from './types'; import { RedisCache } from './redis-cache';
import type { CacheOptions, CacheProvider } from './types';
// Cache instances registry to prevent multiple instances with same prefix // Cache instances registry to prevent multiple instances with same prefix
const cacheInstances = new Map<string, CacheProvider>(); const cacheInstances = new Map<string, CacheProvider>();
@ -14,7 +14,7 @@ export function createCache(options: Partial<CacheOptions> = {}): CacheProvider
ttl: 3600, // 1 hour default ttl: 3600, // 1 hour default
enableMetrics: true, enableMetrics: true,
shared: true, // Default to shared connections shared: true, // Default to shared connections
...options ...options,
}; };
// For shared connections, reuse cache instances with the same key prefix // For shared connections, reuse cache instances with the same key prefix
@ -43,7 +43,7 @@ export function createTradingCache(options: Partial<CacheOptions> = {}): CachePr
ttl: 3600, // 1 hour default ttl: 3600, // 1 hour default
enableMetrics: true, enableMetrics: true,
shared: true, shared: true,
...options ...options,
}); });
} }
@ -56,7 +56,7 @@ export function createMarketDataCache(options: Partial<CacheOptions> = {}): Cach
ttl: 300, // 5 minutes for market data ttl: 300, // 5 minutes for market data
enableMetrics: true, enableMetrics: true,
shared: true, shared: true,
...options ...options,
}); });
} }
@ -69,7 +69,7 @@ export function createIndicatorCache(options: Partial<CacheOptions> = {}): Cache
ttl: 1800, // 30 minutes for indicators ttl: 1800, // 30 minutes for indicators
enableMetrics: true, enableMetrics: true,
shared: true, shared: true,
...options ...options,
}); });
} }
@ -80,13 +80,12 @@ export type {
CacheConfig, CacheConfig,
CacheStats, CacheStats,
CacheKey, CacheKey,
SerializationOptions SerializationOptions,
} from './types'; } from './types';
export { RedisCache } from './redis-cache'; export { RedisCache } from './redis-cache';
export { RedisConnectionManager } from './connection-manager'; export { RedisConnectionManager } from './connection-manager';
export { CacheKeyGenerator } from './key-generator'; export { CacheKeyGenerator } from './key-generator';
// Default export for convenience // Default export for convenience
export default createCache; export default createCache;

View file

@ -65,7 +65,7 @@ export class CacheKeyGenerator {
let hash = 0; let hash = 0;
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i); const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char; hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer hash = hash & hash; // Convert to 32-bit integer
} }
return Math.abs(hash).toString(36); return Math.abs(hash).toString(36);

View file

@ -1,7 +1,7 @@
import Redis from 'ioredis'; import Redis from 'ioredis';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { CacheProvider, CacheOptions, CacheStats } from './types';
import { RedisConnectionManager } from './connection-manager'; import { RedisConnectionManager } from './connection-manager';
import { CacheOptions, CacheProvider, CacheStats } from './types';
/** /**
* Simplified Redis-based cache provider using connection manager * Simplified Redis-based cache provider using connection manager
@ -22,7 +22,7 @@ export class RedisCache implements CacheProvider {
errors: 0, errors: 0,
hitRate: 0, hitRate: 0,
total: 0, total: 0,
uptime: 0 uptime: 0,
}; };
constructor(options: CacheOptions = {}) { constructor(options: CacheOptions = {}) {
@ -34,7 +34,13 @@ export class RedisCache implements CacheProvider {
this.connectionManager = RedisConnectionManager.getInstance(); this.connectionManager = RedisConnectionManager.getInstance();
// Generate connection name based on cache type // Generate connection name based on cache type
const baseName = options.name || this.keyPrefix.replace(':', '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || 'CACHE'; const baseName =
options.name ||
this.keyPrefix
.replace(':', '')
.replace(/[^a-zA-Z0-9]/g, '')
.toUpperCase() ||
'CACHE';
// Get Redis connection (shared by default for cache) // Get Redis connection (shared by default for cache)
this.redis = this.connectionManager.getConnection({ this.redis = this.connectionManager.getConnection({
@ -110,7 +116,7 @@ export class RedisCache implements CacheProvider {
return await operation(); return await operation();
} catch (error) { } catch (error) {
this.logger.error(`Redis ${operationName} failed`, { this.logger.error(`Redis ${operationName} failed`, {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error),
}); });
this.updateStats(false, true); this.updateStats(false, true);
return fallback; return fallback;
@ -144,20 +150,26 @@ export class RedisCache implements CacheProvider {
); );
} }
async set<T>(key: string, value: T, options?: number | { async set<T>(
key: string,
value: T,
options?:
| number
| {
ttl?: number; ttl?: number;
preserveTTL?: boolean; preserveTTL?: boolean;
onlyIfExists?: boolean; onlyIfExists?: boolean;
onlyIfNotExists?: boolean; onlyIfNotExists?: boolean;
getOldValue?: boolean; getOldValue?: boolean;
}): Promise<T | null> { }
): Promise<T | null> {
return this.safeExecute( return this.safeExecute(
async () => { async () => {
const fullKey = this.getKey(key); const fullKey = this.getKey(key);
const serialized = typeof value === 'string' ? value : JSON.stringify(value); const serialized = typeof value === 'string' ? value : JSON.stringify(value);
// Handle backward compatibility - if options is a number, treat as TTL // Handle backward compatibility - if options is a number, treat as TTL
const config = typeof options === 'number' ? { ttl: options } : (options || {}); const config = typeof options === 'number' ? { ttl: options } : options || {};
let oldValue: T | null = null; let oldValue: T | null = null;
@ -180,7 +192,9 @@ export class RedisCache implements CacheProvider {
if (currentTTL === -2) { if (currentTTL === -2) {
// Key doesn't exist // Key doesn't exist
if (config.onlyIfExists) { if (config.onlyIfExists) {
this.logger.debug('Set skipped - key does not exist and onlyIfExists is true', { key }); this.logger.debug('Set skipped - key does not exist and onlyIfExists is true', {
key,
});
return oldValue; return oldValue;
} }
// Set with default or specified TTL // Set with default or specified TTL
@ -279,7 +293,7 @@ export class RedisCache implements CacheProvider {
return pong === 'PONG'; return pong === 'PONG';
} catch (error) { } catch (error) {
this.logger.error('Redis health check failed', { this.logger.error('Redis health check failed', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error),
}); });
return false; return false;
} }
@ -288,7 +302,7 @@ export class RedisCache implements CacheProvider {
getStats(): CacheStats { getStats(): CacheStats {
return { return {
...this.stats, ...this.stats,
uptime: Date.now() - this.startTime uptime: Date.now() - this.startTime,
}; };
} }
@ -308,7 +322,7 @@ export class RedisCache implements CacheProvider {
resolve(); resolve();
}); });
this.redis.once('error', (error) => { this.redis.once('error', error => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
reject(error); reject(error);
}); });
@ -334,7 +348,7 @@ export class RedisCache implements CacheProvider {
async setIfExists<T>(key: string, value: T, ttl?: number): Promise<boolean> { async setIfExists<T>(key: string, value: T, ttl?: number): Promise<boolean> {
const result = await this.set(key, value, { ttl, onlyIfExists: true }); const result = await this.set(key, value, { ttl, onlyIfExists: true });
return result !== null || await this.exists(key); return result !== null || (await this.exists(key));
} }
async setIfNotExists<T>(key: string, value: T, ttl?: number): Promise<boolean> { async setIfNotExists<T>(key: string, value: T, ttl?: number): Promise<boolean> {
@ -347,7 +361,11 @@ export class RedisCache implements CacheProvider {
} }
// Atomic update with transformation // Atomic update with transformation
async updateField<T>(key: string, updater: (current: T | null) => T, ttl?: number): Promise<T | null> { async updateField<T>(
key: string,
updater: (current: T | null) => T,
ttl?: number
): Promise<T | null> {
return this.safeExecute( return this.safeExecute(
async () => { async () => {
const fullKey = this.getKey(key); const fullKey = this.getKey(key);
@ -364,11 +382,10 @@ export class RedisCache implements CacheProvider {
return {current_value, current_ttl} return {current_value, current_ttl}
`; `;
const [currentValue, currentTTL] = await this.redis.eval( const [currentValue, currentTTL] = (await this.redis.eval(luaScript, 1, fullKey)) as [
luaScript, string | null,
1, number,
fullKey ];
) as [string | null, number];
// Parse current value // Parse current value
let parsed: T | null = null; let parsed: T | null = null;

View file

@ -1,12 +1,18 @@
export interface CacheProvider { export interface CacheProvider {
get<T>(key: string): Promise<T | null>; get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, options?: number | { set<T>(
key: string,
value: T,
options?:
| number
| {
ttl?: number; ttl?: number;
preserveTTL?: boolean; preserveTTL?: boolean;
onlyIfExists?: boolean; onlyIfExists?: boolean;
onlyIfNotExists?: boolean; onlyIfNotExists?: boolean;
getOldValue?: boolean; getOldValue?: boolean;
}): Promise<T | null>; }
): Promise<T | null>;
del(key: string): Promise<void>; del(key: string): Promise<void>;
exists(key: string): Promise<boolean>; exists(key: string): Promise<boolean>;
clear(): Promise<void>; clear(): Promise<void>;

View file

@ -61,10 +61,17 @@ export const redisInsightConfig = cleanEnv(process.env, {
REDIS_INSIGHT_PORT: port(8001, 'Redis Insight port'), REDIS_INSIGHT_PORT: port(8001, 'Redis Insight port'),
// Redis Connection Settings // Redis Connection Settings
REDIS_INSIGHT_REDIS_HOSTS: str('local:dragonfly:6379', 'Redis hosts in format name:host:port,name:host:port'), REDIS_INSIGHT_REDIS_HOSTS: str(
'local:dragonfly:6379',
'Redis hosts in format name:host:port,name:host:port'
),
// Configuration // Configuration
REDIS_INSIGHT_LOG_LEVEL: strWithChoices(['error', 'warn', 'info', 'verbose', 'debug'], 'info', 'Redis Insight log level'), REDIS_INSIGHT_LOG_LEVEL: strWithChoices(
['error', 'warn', 'info', 'verbose', 'debug'],
'info',
'Redis Insight log level'
),
REDIS_INSIGHT_DISABLE_ANALYTICS: bool(true, 'Disable analytics collection'), REDIS_INSIGHT_DISABLE_ANALYTICS: bool(true, 'Disable analytics collection'),
REDIS_INSIGHT_BUILD_TYPE: str('DOCKER', 'Build type identifier'), REDIS_INSIGHT_BUILD_TYPE: str('DOCKER', 'Build type identifier'),
}); });

View file

@ -1,8 +1,8 @@
/** /**
* Core configuration module for the Stock Bot platform using Yup * Core configuration module for the Stock Bot platform using Yup
*/ */
import { config as dotenvConfig } from 'dotenv';
import path from 'node:path'; import path from 'node:path';
import { config as dotenvConfig } from 'dotenv';
/** /**
* Represents an error related to configuration validation * Represents an error related to configuration validation
@ -21,7 +21,7 @@ export enum Environment {
Development = 'development', Development = 'development',
Testing = 'testing', Testing = 'testing',
Staging = 'staging', Staging = 'staging',
Production = 'production' Production = 'production',
} }
/** /**
@ -35,11 +35,7 @@ export function loadEnvVariables(envOverride?: string): void {
// 2. .env.{environment} (environment-specific variables) // 2. .env.{environment} (environment-specific variables)
// 3. .env.local (local overrides, not to be committed) // 3. .env.local (local overrides, not to be committed)
const envFiles = [ const envFiles = ['.env', `.env.${env}`, '.env.local'];
'.env',
`.env.${env}`,
'.env.local'
];
for (const file of envFiles) { for (const file of envFiles) {
dotenvConfig({ path: path.resolve(process.cwd(), file) }); dotenvConfig({ path: path.resolve(process.cwd(), file) });
@ -63,6 +59,5 @@ export function getEnvironment(): Environment {
return Environment.Production; return Environment.Production;
default: default:
return Environment.Development; return Environment.Development;
} }
} }

View file

@ -23,7 +23,11 @@ export interface ProviderConfig {
*/ */
export const dataProvidersConfig = cleanEnv(process.env, { export const dataProvidersConfig = cleanEnv(process.env, {
// Default Provider // Default Provider
DEFAULT_DATA_PROVIDER: strWithChoices(['alpaca', 'polygon', 'yahoo', 'iex'], 'alpaca', 'Default data provider'), DEFAULT_DATA_PROVIDER: strWithChoices(
['alpaca', 'polygon', 'yahoo', 'iex'],
'alpaca',
'Default data provider'
),
// Alpaca Configuration // Alpaca Configuration
ALPACA_API_KEY: str('', 'Alpaca API key'), ALPACA_API_KEY: str('', 'Alpaca API key'),
@ -78,8 +82,8 @@ export function getProviderConfig(providerName: string) {
apiKey: dataProvidersConfig.ALPACA_API_KEY, apiKey: dataProvidersConfig.ALPACA_API_KEY,
apiSecret: dataProvidersConfig.ALPACA_API_SECRET, apiSecret: dataProvidersConfig.ALPACA_API_SECRET,
rateLimits: { rateLimits: {
maxRequestsPerMinute: dataProvidersConfig.ALPACA_RATE_LIMIT maxRequestsPerMinute: dataProvidersConfig.ALPACA_RATE_LIMIT,
} },
}; };
case 'POLYGON': case 'POLYGON':
@ -90,8 +94,8 @@ export function getProviderConfig(providerName: string) {
baseUrl: dataProvidersConfig.POLYGON_BASE_URL, baseUrl: dataProvidersConfig.POLYGON_BASE_URL,
apiKey: dataProvidersConfig.POLYGON_API_KEY, apiKey: dataProvidersConfig.POLYGON_API_KEY,
rateLimits: { rateLimits: {
maxRequestsPerMinute: dataProvidersConfig.POLYGON_RATE_LIMIT maxRequestsPerMinute: dataProvidersConfig.POLYGON_RATE_LIMIT,
} },
}; };
case 'YAHOO': case 'YAHOO':
@ -101,8 +105,8 @@ export function getProviderConfig(providerName: string) {
enabled: dataProvidersConfig.YAHOO_ENABLED, enabled: dataProvidersConfig.YAHOO_ENABLED,
baseUrl: dataProvidersConfig.YAHOO_BASE_URL, baseUrl: dataProvidersConfig.YAHOO_BASE_URL,
rateLimits: { rateLimits: {
maxRequestsPerHour: dataProvidersConfig.YAHOO_RATE_LIMIT maxRequestsPerHour: dataProvidersConfig.YAHOO_RATE_LIMIT,
} },
}; };
case 'IEX': case 'IEX':
@ -113,8 +117,8 @@ export function getProviderConfig(providerName: string) {
baseUrl: dataProvidersConfig.IEX_BASE_URL, baseUrl: dataProvidersConfig.IEX_BASE_URL,
apiKey: dataProvidersConfig.IEX_API_KEY, apiKey: dataProvidersConfig.IEX_API_KEY,
rateLimits: { rateLimits: {
maxRequestsPerSecond: dataProvidersConfig.IEX_RATE_LIMIT maxRequestsPerSecond: dataProvidersConfig.IEX_RATE_LIMIT,
} },
}; };
default: default:
@ -127,9 +131,7 @@ export function getProviderConfig(providerName: string) {
*/ */
export function getEnabledProviders() { export function getEnabledProviders() {
const providers = ['alpaca', 'polygon', 'yahoo', 'iex']; const providers = ['alpaca', 'polygon', 'yahoo', 'iex'];
return providers return providers.map(provider => getProviderConfig(provider)).filter(config => config.enabled);
.map(provider => getProviderConfig(provider))
.filter(config => config.enabled);
} }
/** /**
@ -155,7 +157,6 @@ export class DataProviders {
} }
} }
// Export individual config values for convenience // Export individual config values for convenience
export const { export const {
DEFAULT_DATA_PROVIDER, DEFAULT_DATA_PROVIDER,

View file

@ -1,10 +1,10 @@
/** /**
* Environment validation utilities using Yup * Environment validation utilities using Yup
*/ */
import * as yup from 'yup';
import { config } from 'dotenv';
import { join } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path';
import { config } from 'dotenv';
import * as yup from 'yup';
// Function to find and load environment variables // Function to find and load environment variables
function loadEnvFiles() { function loadEnvFiles() {
@ -59,17 +59,14 @@ export function createEnvSchema(shape: Record<string, any>) {
/** /**
* Validates environment variables against a Yup schema * Validates environment variables against a Yup schema
*/ */
export function validateEnv( export function validateEnv(schema: yup.ObjectSchema<any>, env = process.env): any {
schema: yup.ObjectSchema<any>,
env = process.env
): any {
try { try {
const result = schema.validateSync(env, { abortEarly: false }); const result = schema.validateSync(env, { abortEarly: false });
return result; return result;
} catch (error) { } catch (error) {
if (error instanceof yup.ValidationError) { if (error instanceof yup.ValidationError) {
console.error('❌ Invalid environment variables:'); console.error('❌ Invalid environment variables:');
error.inner.forEach((err) => { error.inner.forEach(err => {
console.error(` ${err.path}: ${err.message}`); console.error(` ${err.path}: ${err.message}`);
}); });
} }
@ -94,20 +91,22 @@ export function loadEnv(path?: string) {
*/ */
export const envValidators = { export const envValidators = {
// String with default // String with default
str: (defaultValue?: string, description?: string) => str: (defaultValue?: string, description?: string) => yup.string().default(defaultValue || ''),
yup.string().default(defaultValue || ''),
// String with choices (enum) // String with choices (enum)
strWithChoices: (choices: string[], defaultValue?: string, description?: string) => strWithChoices: (choices: string[], defaultValue?: string, description?: string) =>
yup.string().oneOf(choices).default(defaultValue || choices[0]), yup
.string()
.oneOf(choices)
.default(defaultValue || choices[0]),
// Required string // Required string
requiredStr: (description?: string) => requiredStr: (description?: string) => yup.string().required('Required'),
yup.string().required('Required'),
// Port number // Port number
port: (defaultValue?: number, description?: string) => port: (defaultValue?: number, description?: string) =>
yup.number() yup
.number()
.integer() .integer()
.min(1) .min(1)
.max(65535) .max(65535)
@ -121,7 +120,8 @@ export const envValidators = {
// Number with default // Number with default
num: (defaultValue?: number, description?: string) => num: (defaultValue?: number, description?: string) =>
yup.number() yup
.number()
.transform((val, originalVal) => { .transform((val, originalVal) => {
if (typeof originalVal === 'string') { if (typeof originalVal === 'string') {
return parseFloat(originalVal); return parseFloat(originalVal);
@ -132,7 +132,8 @@ export const envValidators = {
// Boolean with default // Boolean with default
bool: (defaultValue?: boolean, description?: string) => bool: (defaultValue?: boolean, description?: string) =>
yup.boolean() yup
.boolean()
.transform((val, originalVal) => { .transform((val, originalVal) => {
if (typeof originalVal === 'string') { if (typeof originalVal === 'string') {
return originalVal === 'true' || originalVal === '1'; return originalVal === 'true' || originalVal === '1';
@ -143,11 +144,13 @@ export const envValidators = {
// URL validation // URL validation
url: (defaultValue?: string, description?: string) => url: (defaultValue?: string, description?: string) =>
yup.string().url().default(defaultValue || 'http://localhost'), yup
.string()
.url()
.default(defaultValue || 'http://localhost'),
// Email validation // Email validation
email: (description?: string) => email: (description?: string) => yup.string().email(),
yup.string().email(),
}; };
/** /**

View file

@ -41,7 +41,11 @@ export const mongodbConfig = cleanEnv(process.env, {
// Additional Settings // Additional Settings
MONGODB_RETRY_WRITES: bool(true, 'Enable retryable writes'), MONGODB_RETRY_WRITES: bool(true, 'Enable retryable writes'),
MONGODB_JOURNAL: bool(true, 'Enable write concern journal'), MONGODB_JOURNAL: bool(true, 'Enable write concern journal'),
MONGODB_READ_PREFERENCE: strWithChoices(['primary', 'primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'], 'primary', 'MongoDB read preference'), MONGODB_READ_PREFERENCE: strWithChoices(
['primary', 'primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'],
'primary',
'MongoDB read preference'
),
MONGODB_WRITE_CONCERN: str('majority', 'Write concern level'), MONGODB_WRITE_CONCERN: str('majority', 'Write concern level'),
}); });

View file

@ -47,7 +47,11 @@ export const grafanaConfig = cleanEnv(process.env, {
GRAFANA_SECRET_KEY: str('', 'Grafana secret key for encryption'), GRAFANA_SECRET_KEY: str('', 'Grafana secret key for encryption'),
// Database Settings // Database Settings
GRAFANA_DATABASE_TYPE: strWithChoices(['mysql', 'postgres', 'sqlite3'], 'sqlite3', 'Grafana database type'), GRAFANA_DATABASE_TYPE: strWithChoices(
['mysql', 'postgres', 'sqlite3'],
'sqlite3',
'Grafana database type'
),
GRAFANA_DATABASE_URL: str('', 'Grafana database URL'), GRAFANA_DATABASE_URL: str('', 'Grafana database URL'),
// Feature Flags // Feature Flags

View file

@ -5,8 +5,8 @@
* environment loading, validation across modules, and type exports. * environment loading, validation across modules, and type exports.
*/ */
import { describe, test, expect, beforeEach } from 'bun:test'; import { beforeEach, describe, expect, test } from 'bun:test';
import { setTestEnv, clearEnvVars, getMinimalTestEnv } from '../test/setup'; import { clearEnvVars, getMinimalTestEnv, setTestEnv } from '../test/setup';
describe('Config Library Integration', () => { describe('Config Library Integration', () => {
beforeEach(() => { beforeEach(() => {
@ -14,7 +14,8 @@ describe('Config Library Integration', () => {
// Note: Bun handles module caching differently than Jest // Note: Bun handles module caching differently than Jest
}); });
describe('Complete Configuration Loading', () => { test('should load all configuration modules successfully', async () => { describe('Complete Configuration Loading', () => {
test('should load all configuration modules successfully', async () => {
setTestEnv(getMinimalTestEnv()); setTestEnv(getMinimalTestEnv());
// Import all modules // Import all modules
const [ const [
@ -23,14 +24,14 @@ describe('Config Library Integration', () => {
{ questdbConfig }, { questdbConfig },
{ mongodbConfig }, { mongodbConfig },
{ loggingConfig }, { loggingConfig },
{ riskConfig } { riskConfig },
] = await Promise.all([ ] = await Promise.all([
import('../src/core'), import('../src/core'),
import('../src/postgres'), import('../src/postgres'),
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging'), import('../src/logging'),
import('../src/risk') import('../src/risk'),
]); ]);
// Verify all configs are loaded // Verify all configs are loaded
@ -43,13 +44,15 @@ describe('Config Library Integration', () => {
expect(riskConfig).toBeDefined(); expect(riskConfig).toBeDefined();
// Verify core utilities // Verify core utilities
expect(getEnvironment()).toBe(Environment.Testing); // Should be Testing due to NODE_ENV=test in setup expect(getEnvironment()).toBe(Environment.Testing); // Should be Testing due to NODE_ENV=test in setup
expect(postgresConfig.POSTGRES_HOST).toBe('localhost'); expect(questdbConfig.QUESTDB_HOST).toBe('localhost'); expect(postgresConfig.POSTGRES_HOST).toBe('localhost');
expect(questdbConfig.QUESTDB_HOST).toBe('localhost');
expect(mongodbConfig.MONGODB_HOST).toBe('localhost'); // fix: use correct property expect(mongodbConfig.MONGODB_HOST).toBe('localhost'); // fix: use correct property
expect(loggingConfig.LOG_LEVEL).toBeDefined(); expect(loggingConfig.LOG_LEVEL).toBeDefined();
expect(riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); expect(riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1);
}); test('should handle missing required environment variables gracefully', async () => { });
test('should handle missing required environment variables gracefully', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'test' NODE_ENV: 'test',
// Missing required variables // Missing required variables
}); });
@ -67,23 +70,24 @@ describe('Config Library Integration', () => {
// If it throws, that's also acceptable behavior // If it throws, that's also acceptable behavior
expect(error).toBeDefined(); expect(error).toBeDefined();
} }
}); test('should maintain consistency across environment detection', async () => { });
test('should maintain consistency across environment detection', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'production', NODE_ENV: 'production',
...getMinimalTestEnv() ...getMinimalTestEnv(),
}); });
const [ const [
{ Environment, getEnvironment }, { Environment, getEnvironment },
{ postgresConfig }, { postgresConfig },
{ questdbConfig }, { questdbConfig },
{ mongodbConfig }, { mongodbConfig },
{ loggingConfig } { loggingConfig },
] = await Promise.all([ ] = await Promise.all([
import('../src/core'), import('../src/core'),
import('../src/postgres'), import('../src/postgres'),
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging') import('../src/logging'),
]); ]);
// Note: Due to module caching, environment is set at first import // Note: Due to module caching, environment is set at first import
// All modules should detect the same environment (which will be Testing due to test setup) // All modules should detect the same environment (which will be Testing due to test setup)
@ -95,7 +99,8 @@ describe('Config Library Integration', () => {
}); });
}); });
describe('Main Index Exports', () => { test('should export all configuration objects from index', async () => { describe('Main Index Exports', () => {
test('should export all configuration objects from index', async () => {
setTestEnv(getMinimalTestEnv()); setTestEnv(getMinimalTestEnv());
const config = await import('../src/index'); const config = await import('../src/index');
@ -111,7 +116,8 @@ describe('Config Library Integration', () => {
expect(config.mongodbConfig).toBeDefined(); expect(config.mongodbConfig).toBeDefined();
expect(config.loggingConfig).toBeDefined(); expect(config.loggingConfig).toBeDefined();
expect(config.riskConfig).toBeDefined(); expect(config.riskConfig).toBeDefined();
}); test('should export individual values from index', async () => { });
test('should export individual values from index', async () => {
setTestEnv(getMinimalTestEnv()); setTestEnv(getMinimalTestEnv());
const config = await import('../src/index'); const config = await import('../src/index');
@ -132,7 +138,8 @@ describe('Config Library Integration', () => {
// Logging values // Logging values
expect(config.LOG_LEVEL).toBeDefined(); expect(config.LOG_LEVEL).toBeDefined();
}); test('should maintain type safety in exports', async () => { });
test('should maintain type safety in exports', async () => {
setTestEnv(getMinimalTestEnv()); setTestEnv(getMinimalTestEnv());
const { const {
@ -146,7 +153,8 @@ describe('Config Library Integration', () => {
POSTGRES_HOST, POSTGRES_HOST,
POSTGRES_PORT, POSTGRES_PORT,
QUESTDB_HOST, QUESTDB_HOST,
MONGODB_HOST, RISK_MAX_POSITION_SIZE MONGODB_HOST,
RISK_MAX_POSITION_SIZE,
} = await import('../src/index'); } = await import('../src/index');
// Type checking should pass // Type checking should pass
@ -178,7 +186,7 @@ describe('Config Library Integration', () => {
MONGODB_HOST: 'localhost', MONGODB_HOST: 'localhost',
MONGODB_DATABASE: 'test', MONGODB_DATABASE: 'test',
RISK_MAX_POSITION_SIZE: '0.1', RISK_MAX_POSITION_SIZE: '0.1',
RISK_MAX_DAILY_LOSS: '0.05' RISK_MAX_DAILY_LOSS: '0.05',
}); // All imports should succeed with valid config }); // All imports should succeed with valid config
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([ const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
import('../src/core'), import('../src/core'),
@ -186,7 +194,7 @@ describe('Config Library Integration', () => {
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging'), import('../src/logging'),
import('../src/risk') import('../src/risk'),
]); ]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // default test env expect(core.getEnvironment()).toBe(core.Environment.Testing); // default test env
@ -195,7 +203,8 @@ describe('Config Library Integration', () => {
expect(mongodb.mongodbConfig.MONGODB_HOST).toBe('localhost'); expect(mongodb.mongodbConfig.MONGODB_HOST).toBe('localhost');
expect(logging.loggingConfig.LOG_LEVEL).toBe('info'); // set in test expect(logging.loggingConfig.LOG_LEVEL).toBe('info'); // set in test
expect(risk.riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // from test env expect(risk.riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // from test env
}); test('should accept valid environment variables across all modules', async () => { });
test('should accept valid environment variables across all modules', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'development', NODE_ENV: 'development',
LOG_LEVEL: 'debug', LOG_LEVEL: 'debug',
@ -218,7 +227,7 @@ describe('Config Library Integration', () => {
RISK_MAX_DAILY_LOSS: '0.025', RISK_MAX_DAILY_LOSS: '0.025',
LOG_FORMAT: 'json', LOG_FORMAT: 'json',
LOG_FILE_ENABLED: 'false' LOG_FILE_ENABLED: 'false',
}); });
// All imports should succeed // All imports should succeed
@ -228,7 +237,7 @@ describe('Config Library Integration', () => {
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging'), import('../src/logging'),
import('../src/risk') import('../src/risk'),
]); ]);
// Since this is the first test to set NODE_ENV to development and modules might not be cached yet, // Since this is the first test to set NODE_ENV to development and modules might not be cached yet,
@ -242,7 +251,8 @@ describe('Config Library Integration', () => {
}); });
}); });
describe('Configuration Consistency', () => { test('should maintain consistent SSL settings across databases', async () => { describe('Configuration Consistency', () => {
test('should maintain consistent SSL settings across databases', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'production', NODE_ENV: 'production',
POSTGRES_HOST: 'prod-postgres.com', POSTGRES_HOST: 'prod-postgres.com',
@ -253,29 +263,27 @@ describe('Config Library Integration', () => {
MONGODB_HOST: 'prod-mongo.com', MONGODB_HOST: 'prod-mongo.com',
MONGODB_DATABASE: 'prod_db', MONGODB_DATABASE: 'prod_db',
RISK_MAX_POSITION_SIZE: '0.1', RISK_MAX_POSITION_SIZE: '0.1',
RISK_MAX_DAILY_LOSS: '0.05' RISK_MAX_DAILY_LOSS: '0.05',
// SSL settings not explicitly set - should use defaults // SSL settings not explicitly set - should use defaults
}); });
const [postgres, questdb, mongodb] = await Promise.all([ const [postgres, questdb, mongodb] = await Promise.all([
import('../src/postgres'), import('../src/postgres'),
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb') import('../src/mongodb'),
]); ]);
// Check actual SSL property names and their default values expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default is false // Check actual SSL property names and their default values expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default is false
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); // default is false expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); // default is false
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false); // default is false expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false); // default is false
}); test('should maintain consistent environment detection across modules', async () => { });
test('should maintain consistent environment detection across modules', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'staging', NODE_ENV: 'staging',
...getMinimalTestEnv() ...getMinimalTestEnv(),
}); });
const [core, logging] = await Promise.all([ const [core, logging] = await Promise.all([import('../src/core'), import('../src/logging')]);
import('../src/core'),
import('../src/logging')
]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
// The setTestEnv call above doesn't actually change the real NODE_ENV because modules cache it // The setTestEnv call above doesn't actually change the real NODE_ENV because modules cache it
@ -284,7 +292,8 @@ describe('Config Library Integration', () => {
}); });
}); });
describe('Performance and Caching', () => { test('should cache configuration values between imports', async () => { describe('Performance and Caching', () => {
test('should cache configuration values between imports', async () => {
setTestEnv(getMinimalTestEnv()); setTestEnv(getMinimalTestEnv());
// Import the same module multiple times // Import the same module multiple times
@ -309,7 +318,7 @@ describe('Config Library Integration', () => {
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging'), import('../src/logging'),
import('../src/risk') import('../src/risk'),
]); ]);
const endTime = Date.now(); const endTime = Date.now();
@ -322,7 +331,7 @@ describe('Config Library Integration', () => {
describe('Error Handling and Recovery', () => { describe('Error Handling and Recovery', () => {
test('should provide helpful error messages for missing variables', async () => { test('should provide helpful error messages for missing variables', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'test' NODE_ENV: 'test',
// Missing required variables // Missing required variables
}); });
@ -345,7 +354,8 @@ describe('Config Library Integration', () => {
// If it throws, check that error message is helpful // If it throws, check that error message is helpful
expect((error as Error).message).toBeTruthy(); expect((error as Error).message).toBeTruthy();
} }
}); test('should handle partial configuration failures gracefully', async () => { });
test('should handle partial configuration failures gracefully', async () => {
setTestEnv({ setTestEnv({
NODE_ENV: 'test', NODE_ENV: 'test',
LOG_LEVEL: 'info', LOG_LEVEL: 'info',
@ -355,7 +365,7 @@ describe('Config Library Integration', () => {
POSTGRES_USERNAME: 'test', POSTGRES_USERNAME: 'test',
POSTGRES_PASSWORD: 'test', POSTGRES_PASSWORD: 'test',
// Postgres should work // Postgres should work
QUESTDB_HOST: 'localhost' QUESTDB_HOST: 'localhost',
// QuestDB should work // QuestDB should work
// MongoDB and Risk should work with defaults // MongoDB and Risk should work with defaults
}); });
@ -385,7 +395,7 @@ describe('Config Library Integration', () => {
QUESTDB_TLS_ENABLED: undefined, // Should default to false QUESTDB_TLS_ENABLED: undefined, // Should default to false
MONGODB_TLS: undefined, // Should default to false MONGODB_TLS: undefined, // Should default to false
LOG_FORMAT: undefined, // Should default to json LOG_FORMAT: undefined, // Should default to json
RISK_CIRCUIT_BREAKER_ENABLED: undefined // Should default to true RISK_CIRCUIT_BREAKER_ENABLED: undefined, // Should default to true
}); });
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([ const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
@ -394,11 +404,12 @@ describe('Config Library Integration', () => {
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging'), import('../src/logging'),
import('../src/risk') import('../src/risk'),
]); ]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false);
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false); expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false);
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false);
expect(logging.loggingConfig.LOG_FORMAT).toBe('json'); // default expect(logging.loggingConfig.LOG_FORMAT).toBe('json'); // default
expect(risk.riskConfig.RISK_CIRCUIT_BREAKER_ENABLED).toBe(true); // default expect(risk.riskConfig.RISK_CIRCUIT_BREAKER_ENABLED).toBe(true); // default
}); });
@ -411,7 +422,7 @@ describe('Config Library Integration', () => {
QUESTDB_TLS_ENABLED: undefined, // Should default to false QUESTDB_TLS_ENABLED: undefined, // Should default to false
MONGODB_TLS: undefined, // Should default to false MONGODB_TLS: undefined, // Should default to false
LOG_FORMAT: undefined, // Should default to json LOG_FORMAT: undefined, // Should default to json
RISK_CIRCUIT_BREAKER_ENABLED: undefined // Should default to true RISK_CIRCUIT_BREAKER_ENABLED: undefined, // Should default to true
}); });
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([ const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
@ -420,7 +431,8 @@ describe('Config Library Integration', () => {
import('../src/questdb'), import('../src/questdb'),
import('../src/mongodb'), import('../src/mongodb'),
import('../src/logging'), import('../src/logging'),
import('../src/risk') ]); import('../src/risk'),
]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default doesn't change by env expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default doesn't change by env

View file

@ -48,14 +48,15 @@ export function clearEnvVars(vars: string[]): void {
*/ */
export function getCleanEnv(): typeof process.env { export function getCleanEnv(): typeof process.env {
return { return {
NODE_ENV: 'test' NODE_ENV: 'test',
}; };
} }
/** /**
* Helper function to create minimal required environment variables * Helper function to create minimal required environment variables
*/ */
export function getMinimalTestEnv(): Record<string, string> { return { export function getMinimalTestEnv(): Record<string, string> {
return {
NODE_ENV: 'test', NODE_ENV: 'test',
// Logging // Logging
LOG_LEVEL: 'info', // Changed from 'error' to 'info' to match test expectations LOG_LEVEL: 'info', // Changed from 'error' to 'info' to match test expectations
@ -87,6 +88,6 @@ export function getMinimalTestEnv(): Record<string, string> { return {
RISK_MAX_POSITION_SIZE: '0.1', RISK_MAX_POSITION_SIZE: '0.1',
RISK_MAX_DAILY_LOSS: '0.05', RISK_MAX_DAILY_LOSS: '0.05',
// Admin // Admin
ADMIN_PORT: '8080' ADMIN_PORT: '8080',
}; };
} }

View file

@ -109,7 +109,7 @@ export class DataFrame {
return new DataFrame(this.data.slice(0, n), { return new DataFrame(this.data.slice(0, n), {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -117,7 +117,7 @@ export class DataFrame {
return new DataFrame(this.data.slice(-n), { return new DataFrame(this.data.slice(-n), {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -126,7 +126,7 @@ export class DataFrame {
return new DataFrame(slice, { return new DataFrame(slice, {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -151,7 +151,7 @@ export class DataFrame {
return new DataFrame(newData, { return new DataFrame(newData, {
columns: validColumns, columns: validColumns,
index: this._index, index: this._index,
dtypes: this.filterDtypes(validColumns) dtypes: this.filterDtypes(validColumns),
}); });
} }
@ -174,17 +174,15 @@ export class DataFrame {
const newData = this.data.map((row, index) => ({ const newData = this.data.map((row, index) => ({
...row, ...row,
[column]: values[index] [column]: values[index],
})); }));
const newColumns = this._columns.includes(column) const newColumns = this._columns.includes(column) ? this._columns : [...this._columns, column];
? this._columns
: [...this._columns, column];
return new DataFrame(newData, { return new DataFrame(newData, {
columns: newColumns, columns: newColumns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -194,7 +192,7 @@ export class DataFrame {
return new DataFrame(filteredData, { return new DataFrame(filteredData, {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -202,13 +200,20 @@ export class DataFrame {
return this.filter(row => { return this.filter(row => {
const cellValue = row[column]; const cellValue = row[column];
switch (operator) { switch (operator) {
case '>': return cellValue > value; case '>':
case '<': return cellValue < value; return cellValue > value;
case '>=': return cellValue >= value; case '<':
case '<=': return cellValue <= value; return cellValue < value;
case '==': return cellValue === value; case '>=':
case '!=': return cellValue !== value; return cellValue >= value;
default: return false; case '<=':
return cellValue <= value;
case '==':
return cellValue === value;
case '!=':
return cellValue !== value;
default:
return false;
} }
}); });
} }
@ -228,7 +233,7 @@ export class DataFrame {
return new DataFrame(sortedData, { return new DataFrame(sortedData, {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -249,7 +254,7 @@ export class DataFrame {
result[key] = new DataFrame(rows, { result[key] = new DataFrame(rows, {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
} }
@ -333,7 +338,7 @@ export class DataFrame {
const tempDf = new DataFrame(rows, { const tempDf = new DataFrame(rows, {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
// Create OHLCV aggregation // Create OHLCV aggregation
@ -343,7 +348,7 @@ export class DataFrame {
high: tempDf.max('high') || tempDf.max('close') || tempDf.max('price'), high: tempDf.max('high') || tempDf.max('close') || tempDf.max('price'),
low: tempDf.min('low') || tempDf.min('close') || tempDf.min('price'), low: tempDf.min('low') || tempDf.min('close') || tempDf.min('price'),
close: rows[rows.length - 1].close || rows[rows.length - 1].price, close: rows[rows.length - 1].close || rows[rows.length - 1].price,
volume: tempDf.sum('volume') || 0 volume: tempDf.sum('volume') || 0,
}; };
aggregatedData.push(aggregated); aggregatedData.push(aggregated);
@ -371,7 +376,7 @@ export class DataFrame {
const tempDf = new DataFrame(rows, { const tempDf = new DataFrame(rows, {
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: this._dtypes dtypes: this._dtypes,
}); });
const aggregated: DataFrameRow = { const aggregated: DataFrameRow = {
@ -380,7 +385,7 @@ export class DataFrame {
high: tempDf.max('high') || tempDf.max('close') || tempDf.max('price'), high: tempDf.max('high') || tempDf.max('close') || tempDf.max('price'),
low: tempDf.min('low') || tempDf.min('close') || tempDf.min('price'), low: tempDf.min('low') || tempDf.min('close') || tempDf.min('price'),
close: rows[rows.length - 1].close || rows[rows.length - 1].price, close: rows[rows.length - 1].close || rows[rows.length - 1].price,
volume: tempDf.sum('volume') || 0 volume: tempDf.sum('volume') || 0,
}; };
aggregatedData.push(aggregated); aggregatedData.push(aggregated);
@ -391,11 +396,14 @@ export class DataFrame {
// Utility methods // Utility methods
copy(): DataFrame { copy(): DataFrame {
return new DataFrame(this.data.map(row => ({ ...row })), { return new DataFrame(
this.data.map(row => ({ ...row })),
{
columns: this._columns, columns: this._columns,
index: this._index, index: this._index,
dtypes: { ...this._dtypes } dtypes: { ...this._dtypes },
}); }
);
} }
concat(other: DataFrame): DataFrame { concat(other: DataFrame): DataFrame {
@ -405,7 +413,7 @@ export class DataFrame {
return new DataFrame(combinedData, { return new DataFrame(combinedData, {
columns: combinedColumns, columns: combinedColumns,
index: this._index, index: this._index,
dtypes: { ...this._dtypes, ...other._dtypes } dtypes: { ...this._dtypes, ...other._dtypes },
}); });
} }
@ -417,7 +425,9 @@ export class DataFrame {
return JSON.stringify(this.data); return JSON.stringify(this.data);
} }
private filterDtypes(columns: string[]): Record<string, 'number' | 'string' | 'boolean' | 'date'> { private filterDtypes(
columns: string[]
): Record<string, 'number' | 'string' | 'boolean' | 'date'> {
const filtered: Record<string, 'number' | 'string' | 'boolean' | 'date'> = {}; const filtered: Record<string, 'number' | 'string' | 'boolean' | 'date'> = {};
for (const col of columns) { for (const col of columns) {
if (this._dtypes[col]) { if (this._dtypes[col]) {
@ -480,6 +490,6 @@ export function readCSV(csvData: string, options?: DataFrameOptions): DataFrame
return new DataFrame(data, { return new DataFrame(data, {
columns: headers, columns: headers,
...options ...options,
}); });
} }

View file

@ -1,7 +1,7 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { getLogger } from '@stock-bot/logger';
import { dragonflyConfig } from '@stock-bot/config'; import { dragonflyConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
export interface EventBusMessage { export interface EventBusMessage {
id: string; id: string;
@ -72,7 +72,9 @@ export class EventBus extends EventEmitter {
this.subscriber.on('message', this.handleRedisMessage.bind(this)); this.subscriber.on('message', this.handleRedisMessage.bind(this));
} }
this.logger.info(`Redis event bus initialized (mode: ${this.useStreams ? 'streams' : 'pub/sub'})`); this.logger.info(
`Redis event bus initialized (mode: ${this.useStreams ? 'streams' : 'pub/sub'})`
);
} }
private handleRedisMessage(channel: string, message: string) { private handleRedisMessage(channel: string, message: string) {
@ -90,7 +92,11 @@ export class EventBus extends EventEmitter {
} }
} }
async publish<T = any>(type: string, data: T, metadata?: Record<string, any>): Promise<string | null> { async publish<T = any>(
type: string,
data: T,
metadata?: Record<string, any>
): Promise<string | null> {
const message: EventBusMessage = { const message: EventBusMessage = {
id: this.generateId(), id: this.generateId(),
type, type,
@ -109,17 +115,23 @@ export class EventBus extends EventEmitter {
const messageId = await this.redis.xadd( const messageId = await this.redis.xadd(
streamKey, streamKey,
'*', '*',
'id', message.id, 'id',
'type', message.type, message.id,
'source', message.source, 'type',
'timestamp', message.timestamp.toString(), message.type,
'data', JSON.stringify(message.data), 'source',
'metadata', JSON.stringify(message.metadata || {}) message.source,
'timestamp',
message.timestamp.toString(),
'data',
JSON.stringify(message.data),
'metadata',
JSON.stringify(message.metadata || {})
); );
this.logger.debug(`Published event to stream: ${type}`, { this.logger.debug(`Published event to stream: ${type}`, {
messageId, messageId,
streamId: messageId streamId: messageId,
}); });
return messageId as string; return messageId as string;
} else { } else {
@ -156,7 +168,10 @@ export class EventBus extends EventEmitter {
} }
} }
private async subscribeToStream<T = any>(eventType: string, handler: EventHandler<T>): Promise<void> { private async subscribeToStream<T = any>(
eventType: string,
handler: EventHandler<T>
): Promise<void> {
const streamKey = `events:${eventType}`; const streamKey = `events:${eventType}`;
const groupName = `${eventType}-consumers`; const groupName = `${eventType}-consumers`;
const consumerName = `${this.serviceName}-${Date.now()}`; const consumerName = `${this.serviceName}-${Date.now()}`;
@ -194,10 +209,16 @@ export class EventBus extends EventEmitter {
await this.claimPendingMessages(streamKey, groupName, consumerName, handler); await this.claimPendingMessages(streamKey, groupName, consumerName, handler);
const messages = await this.redis.xreadgroup( const messages = await this.redis.xreadgroup(
'GROUP', groupName, consumerName, 'GROUP',
'COUNT', 10, groupName,
'BLOCK', 1000, consumerName,
'STREAMS', streamKey, '>' 'COUNT',
10,
'BLOCK',
1000,
'STREAMS',
streamKey,
'>'
); );
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
@ -264,14 +285,16 @@ export class EventBus extends EventEmitter {
this.logger.debug(`Processed stream message: ${msgId}`, { this.logger.debug(`Processed stream message: ${msgId}`, {
eventType: message.type, eventType: message.type,
source: message.source source: message.source,
}); });
return; return;
} catch (error) { } catch (error) {
retryCount++; retryCount++;
this.logger.error(`Error processing stream message ${msgId} (attempt ${retryCount}):`, error); this.logger.error(
`Error processing stream message ${msgId} (attempt ${retryCount}):`,
error
);
if (retryCount >= this.maxRetries) { if (retryCount >= this.maxRetries) {
await this.moveToDeadLetterQueue(msgId, fields, streamKey, groupName, error); await this.moveToDeadLetterQueue(msgId, fields, streamKey, groupName, error);
@ -290,13 +313,13 @@ export class EventBus extends EventEmitter {
handler: EventHandler handler: EventHandler
): Promise<void> { ): Promise<void> {
try { try {
const pendingMessages = await this.redis.xpending( const pendingMessages = (await this.redis.xpending(
streamKey, streamKey,
groupName, groupName,
'-', '-',
'+', '+',
10 10
) as any[]; )) as any[];
if (!pendingMessages || pendingMessages.length === 0) { if (!pendingMessages || pendingMessages.length === 0) {
return; return;
@ -311,13 +334,13 @@ export class EventBus extends EventEmitter {
} }
const messageIds = oldMessages.map((msg: any[]) => msg[0]); const messageIds = oldMessages.map((msg: any[]) => msg[0]);
const claimedMessages = await this.redis.xclaim( const claimedMessages = (await this.redis.xclaim(
streamKey, streamKey,
groupName, groupName,
consumerName, consumerName,
60000, 60000,
...messageIds ...messageIds
) as [string, string[]][]; )) as [string, string[]][];
for (const [msgId, fields] of claimedMessages) { for (const [msgId, fields] of claimedMessages) {
await this.processStreamMessage(msgId, fields, streamKey, groupName, handler); await this.processStreamMessage(msgId, fields, streamKey, groupName, handler);
@ -343,22 +366,35 @@ export class EventBus extends EventEmitter {
await this.redis.xadd( await this.redis.xadd(
dlqKey, dlqKey,
'*', '*',
'original_id', msgId, 'original_id',
'original_stream', streamKey, msgId,
'error', (error as Error).message || 'Unknown error', 'original_stream',
'timestamp', Date.now().toString(), streamKey,
'id', message.id, 'error',
'type', message.type, (error as Error).message || 'Unknown error',
'source', message.source, 'timestamp',
'data', JSON.stringify(message.data), Date.now().toString(),
'metadata', JSON.stringify(message.metadata || {}) 'id',
message.id,
'type',
message.type,
'source',
message.source,
'data',
JSON.stringify(message.data),
'metadata',
JSON.stringify(message.metadata || {})
); );
await this.redis.xack(streamKey, groupName, msgId); await this.redis.xack(streamKey, groupName, msgId);
this.logger.warn(`Moved message ${msgId} to dead letter queue: ${dlqKey}`, { error: (error as Error).message }); this.logger.warn(`Moved message ${msgId} to dead letter queue: ${dlqKey}`, {
error: (error as Error).message,
});
} catch (dlqError) { } catch (dlqError) {
this.logger.error(`Failed to move message ${msgId} to dead letter queue:`, { error: dlqError }); this.logger.error(`Failed to move message ${msgId} to dead letter queue:`, {
error: dlqError,
});
} }
} }
@ -393,8 +429,9 @@ export class EventBus extends EventEmitter {
if (this.enablePersistence) { if (this.enablePersistence) {
try { try {
if (this.useStreams) { if (this.useStreams) {
const consumersToStop = Array.from(this.consumers.entries()) const consumersToStop = Array.from(this.consumers.entries()).filter(([key]) =>
.filter(([key]) => key.startsWith(`${eventType}-`)); key.startsWith(`${eventType}-`)
);
for (const [key, consumerInfo] of consumersToStop) { for (const [key, consumerInfo] of consumersToStop) {
consumerInfo.isRunning = false; consumerInfo.isRunning = false;
@ -479,14 +516,17 @@ export class EventBus extends EventEmitter {
let messages: [string, string[]][]; let messages: [string, string[]][];
if (count) { if (count) {
messages = await this.redis.xrange(streamKey, startId, endId, 'COUNT', count) as [string, string[]][]; messages = (await this.redis.xrange(streamKey, startId, endId, 'COUNT', count)) as [
string,
string[],
][];
} else { } else {
messages = await this.redis.xrange(streamKey, startId, endId) as [string, string[]][]; messages = (await this.redis.xrange(streamKey, startId, endId)) as [string, string[]][];
} }
return messages.map(([id, fields]) => ({ return messages.map(([id, fields]) => ({
...this.parseStreamMessage(fields), ...this.parseStreamMessage(fields),
id id,
})); }));
} catch (error) { } catch (error) {
this.logger.error(`Failed to read stream history for: ${eventType}`, error); this.logger.error(`Failed to read stream history for: ${eventType}`, error);

View file

@ -1,8 +1,8 @@
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'; import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
import type { RequestConfig, HttpResponse } from '../types';
import type { RequestAdapter } from './types';
import { ProxyManager } from '../proxy-manager'; import { ProxyManager } from '../proxy-manager';
import type { HttpResponse, RequestConfig } from '../types';
import { HttpError } from '../types'; import { HttpError } from '../types';
import type { RequestAdapter } from './types';
/** /**
* Axios adapter for SOCKS proxies * Axios adapter for SOCKS proxies
@ -10,7 +10,9 @@ import { HttpError } from '../types';
export class AxiosAdapter implements RequestAdapter { export class AxiosAdapter implements RequestAdapter {
canHandle(config: RequestConfig): boolean { canHandle(config: RequestConfig): boolean {
// Axios handles SOCKS proxies // Axios handles SOCKS proxies
return Boolean(config.proxy && (config.proxy.protocol === 'socks4' || config.proxy.protocol === 'socks5')); return Boolean(
config.proxy && (config.proxy.protocol === 'socks4' || config.proxy.protocol === 'socks5')
);
} }
async request<T = any>(config: RequestConfig, signal: AbortSignal): Promise<HttpResponse<T>> { async request<T = any>(config: RequestConfig, signal: AbortSignal): Promise<HttpResponse<T>> {
@ -30,7 +32,8 @@ export class AxiosAdapter implements RequestAdapter {
signal, signal,
// Don't throw on non-2xx status codes - let caller handle // Don't throw on non-2xx status codes - let caller handle
validateStatus: () => true, validateStatus: () => true,
}; const response: AxiosResponse<T> = await axios(axiosConfig); };
const response: AxiosResponse<T> = await axios(axiosConfig);
const httpResponse: HttpResponse<T> = { const httpResponse: HttpResponse<T> = {
data: response.data, data: response.data,

View file

@ -1,7 +1,7 @@
import type { RequestConfig } from '../types'; import type { RequestConfig } from '../types';
import type { RequestAdapter } from './types';
import { FetchAdapter } from './fetch-adapter';
import { AxiosAdapter } from './axios-adapter'; import { AxiosAdapter } from './axios-adapter';
import { FetchAdapter } from './fetch-adapter';
import type { RequestAdapter } from './types';
/** /**
* Factory for creating the appropriate request adapter * Factory for creating the appropriate request adapter

View file

@ -1,7 +1,7 @@
import type { RequestConfig, HttpResponse } from '../types';
import type { RequestAdapter } from './types';
import { ProxyManager } from '../proxy-manager'; import { ProxyManager } from '../proxy-manager';
import type { HttpResponse, RequestConfig } from '../types';
import { HttpError } from '../types'; import { HttpError } from '../types';
import type { RequestAdapter } from './types';
/** /**
* Fetch adapter for HTTP/HTTPS proxies and non-proxy requests * Fetch adapter for HTTP/HTTPS proxies and non-proxy requests
@ -33,16 +33,17 @@ export class FetchAdapter implements RequestAdapter {
// Add proxy if needed (using Bun's built-in proxy support) // Add proxy if needed (using Bun's built-in proxy support)
if (proxy) { if (proxy) {
(fetchOptions as any).proxy = ProxyManager.createProxyUrl(proxy); (fetchOptions as any).proxy = ProxyManager.createProxyUrl(proxy);
} const response = await fetch(url, fetchOptions); }
const response = await fetch(url, fetchOptions);
// Parse response based on content type // Parse response based on content type
let responseData: T; let responseData: T;
const contentType = response.headers.get('content-type') || ''; const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) { if (contentType.includes('application/json')) {
responseData = await response.json() as T; responseData = (await response.json()) as T;
} else { } else {
responseData = await response.text() as T; responseData = (await response.text()) as T;
} }
const httpResponse: HttpResponse<T> = { const httpResponse: HttpResponse<T> = {

View file

@ -1,4 +1,4 @@
import type { RequestConfig, HttpResponse } from '../types'; import type { HttpResponse, RequestConfig } from '../types';
/** /**
* Request adapter interface for different HTTP implementations * Request adapter interface for different HTTP implementations

View file

@ -1,12 +1,8 @@
import type { Logger } from '@stock-bot/logger'; import type { Logger } from '@stock-bot/logger';
import type {
HttpClientConfig,
RequestConfig,
HttpResponse,
} from './types';
import { HttpError } from './types';
import { ProxyManager } from './proxy-manager';
import { AdapterFactory } from './adapters/index'; import { AdapterFactory } from './adapters/index';
import { ProxyManager } from './proxy-manager';
import type { HttpClientConfig, HttpResponse, RequestConfig } from './types';
import { HttpError } from './types';
export class HttpClient { export class HttpClient {
private readonly config: HttpClientConfig; private readonly config: HttpClientConfig;
@ -18,23 +14,41 @@ export class HttpClient {
} }
// Convenience methods // Convenience methods
async get<T = any>(url: string, config: Omit<RequestConfig, 'method' | 'url'> = {}): Promise<HttpResponse<T>> { async get<T = any>(
url: string,
config: Omit<RequestConfig, 'method' | 'url'> = {}
): Promise<HttpResponse<T>> {
return this.request<T>({ ...config, method: 'GET', url }); return this.request<T>({ ...config, method: 'GET', url });
} }
async post<T = any>(url: string, data?: any, config: Omit<RequestConfig, 'method' | 'url' | 'data'> = {}): Promise<HttpResponse<T>> { async post<T = any>(
url: string,
data?: any,
config: Omit<RequestConfig, 'method' | 'url' | 'data'> = {}
): Promise<HttpResponse<T>> {
return this.request<T>({ ...config, method: 'POST', url, data }); return this.request<T>({ ...config, method: 'POST', url, data });
} }
async put<T = any>(url: string, data?: any, config: Omit<RequestConfig, 'method' | 'url' | 'data'> = {}): Promise<HttpResponse<T>> { async put<T = any>(
url: string,
data?: any,
config: Omit<RequestConfig, 'method' | 'url' | 'data'> = {}
): Promise<HttpResponse<T>> {
return this.request<T>({ ...config, method: 'PUT', url, data }); return this.request<T>({ ...config, method: 'PUT', url, data });
} }
async del<T = any>(url: string, config: Omit<RequestConfig, 'method' | 'url'> = {}): Promise<HttpResponse<T>> { async del<T = any>(
url: string,
config: Omit<RequestConfig, 'method' | 'url'> = {}
): Promise<HttpResponse<T>> {
return this.request<T>({ ...config, method: 'DELETE', url }); return this.request<T>({ ...config, method: 'DELETE', url });
} }
async patch<T = any>(url: string, data?: any, config: Omit<RequestConfig, 'method' | 'url' | 'data'> = {}): Promise<HttpResponse<T>> { async patch<T = any>(
url: string,
data?: any,
config: Omit<RequestConfig, 'method' | 'url' | 'data'> = {}
): Promise<HttpResponse<T>> {
return this.request<T>({ ...config, method: 'PATCH', url, data }); return this.request<T>({ ...config, method: 'PATCH', url, data });
} }
@ -48,7 +62,7 @@ export class HttpClient {
this.logger?.debug('Making HTTP request', { this.logger?.debug('Making HTTP request', {
method: finalConfig.method, method: finalConfig.method,
url: finalConfig.url, url: finalConfig.url,
hasProxy: !!finalConfig.proxy hasProxy: !!finalConfig.proxy,
}); });
try { try {
@ -98,7 +112,7 @@ export class HttpClient {
url: config.url, url: config.url,
method: config.method, method: config.method,
timeout, timeout,
elapsed elapsed,
}); });
// Attempt to abort (may or may not work with Bun) // Attempt to abort (may or may not work with Bun)
@ -115,17 +129,23 @@ export class HttpClient {
const response = await Promise.race([ const response = await Promise.race([
adapter.request<T>(config, controller.signal), adapter.request<T>(config, controller.signal),
timeoutPromise timeoutPromise,
]); ]);
this.logger?.debug('Adapter request successful', { url: config.url, elapsedMs: Date.now() - startTime }); this.logger?.debug('Adapter request successful', {
url: config.url,
elapsedMs: Date.now() - startTime,
});
// Clear timeout on success // Clear timeout on success
clearTimeout(timeoutId); clearTimeout(timeoutId);
return response; return response;
} catch (error) { } catch (error) {
const elapsed = Date.now() - startTime; const elapsed = Date.now() - startTime;
this.logger?.debug('Adapter failed successful', { url: config.url, elapsedMs: Date.now() - startTime }); this.logger?.debug('Adapter failed successful', {
url: config.url,
elapsedMs: Date.now() - startTime,
});
clearTimeout(timeoutId); clearTimeout(timeoutId);
// Handle timeout // Handle timeout

View file

@ -1,7 +1,7 @@
import axios, { AxiosRequestConfig, type AxiosInstance } from 'axios'; import axios, { AxiosRequestConfig, type AxiosInstance } from 'axios';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
import type { ProxyInfo } from './types'; import type { ProxyInfo } from './types';
export class ProxyManager { export class ProxyManager {

View file

@ -1,4 +1,4 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { HttpClient, HttpError } from '../src/index'; import { HttpClient, HttpError } from '../src/index';
import { MockServer } from './mock-server'; import { MockServer } from './mock-server';
@ -25,7 +25,7 @@ describe('HTTP Integration Tests', () => {
beforeAll(() => { beforeAll(() => {
client = new HttpClient({ client = new HttpClient({
timeout: 10000 timeout: 10000,
}); });
}); });
@ -51,7 +51,10 @@ describe('HTTP Integration Tests', () => {
expect(Array.isArray(response.data)).toBe(true); expect(Array.isArray(response.data)).toBe(true);
expect(response.data.length).toBeGreaterThan(0); expect(response.data.length).toBeGreaterThan(0);
} catch (error) { } catch (error) {
console.warn('Large response test skipped due to network issues:', (error as Error).message); console.warn(
'Large response test skipped due to network issues:',
(error as Error).message
);
} }
}); });
@ -60,7 +63,7 @@ describe('HTTP Integration Tests', () => {
const postData = { const postData = {
title: 'Integration Test Post', title: 'Integration Test Post',
body: 'This is a test post from integration tests', body: 'This is a test post from integration tests',
userId: 1 userId: 1,
}; };
const response = await client.post('https://jsonplaceholder.typicode.com/posts', postData); const response = await client.post('https://jsonplaceholder.typicode.com/posts', postData);
@ -69,12 +72,16 @@ describe('HTTP Integration Tests', () => {
expect(response.data).toHaveProperty('id'); expect(response.data).toHaveProperty('id');
expect(response.data.title).toBe(postData.title); expect(response.data.title).toBe(postData.title);
} catch (error) { } catch (error) {
console.warn('POST integration test skipped due to network issues:', (error as Error).message); console.warn(
'POST integration test skipped due to network issues:',
(error as Error).message
);
} }
}); });
}); });
describe('Error scenarios with mock server', () => { test('should handle various HTTP status codes', async () => { describe('Error scenarios with mock server', () => {
test('should handle various HTTP status codes', async () => {
const successCodes = [200, 201]; const successCodes = [200, 201];
const errorCodes = [400, 401, 403, 404, 500, 503]; const errorCodes = [400, 401, 403, 404, 500, 503];
@ -86,9 +93,9 @@ describe('HTTP Integration Tests', () => {
// Test error codes (should throw HttpError) // Test error codes (should throw HttpError)
for (const statusCode of errorCodes) { for (const statusCode of errorCodes) {
await expect( await expect(client.get(`${mockServerBaseUrl}/status/${statusCode}`)).rejects.toThrow(
client.get(`${mockServerBaseUrl}/status/${statusCode}`) HttpError
).rejects.toThrow(HttpError); );
} }
}); });
@ -102,7 +109,7 @@ describe('HTTP Integration Tests', () => {
test('should handle concurrent requests', async () => { test('should handle concurrent requests', async () => {
const requests = Array.from({ length: 5 }, (_, i) => const requests = Array.from({ length: 5 }, (_, i) =>
client.get(`${mockServerBaseUrl}/`, { client.get(`${mockServerBaseUrl}/`, {
headers: { 'X-Request-ID': `req-${i}` } headers: { 'X-Request-ID': `req-${i}` },
}) })
); );
@ -137,7 +144,7 @@ describe('HTTP Integration Tests', () => {
test('should maintain connection efficiency', async () => { test('should maintain connection efficiency', async () => {
const clientWithKeepAlive = new HttpClient({ const clientWithKeepAlive = new HttpClient({
timeout: 5000 timeout: 5000,
}); });
const requests = Array.from({ length: 3 }, () => const requests = Array.from({ length: 3 }, () =>

View file

@ -1,4 +1,4 @@
import { describe, test, expect, beforeEach, beforeAll, afterAll } from 'bun:test'; import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
import { HttpClient, HttpError, ProxyManager } from '../src/index'; import { HttpClient, HttpError, ProxyManager } from '../src/index';
import type { ProxyInfo } from '../src/types'; import type { ProxyInfo } from '../src/types';
import { MockServer } from './mock-server'; import { MockServer } from './mock-server';
@ -56,11 +56,11 @@ describe('HttpClient', () => {
test('should handle custom headers', async () => { test('should handle custom headers', async () => {
const customHeaders = { const customHeaders = {
'X-Custom-Header': 'test-value', 'X-Custom-Header': 'test-value',
'User-Agent': 'StockBot-HTTP-Client/1.0' 'User-Agent': 'StockBot-HTTP-Client/1.0',
}; };
const response = await client.get(`${mockServerBaseUrl}/headers`, { const response = await client.get(`${mockServerBaseUrl}/headers`, {
headers: customHeaders headers: customHeaders,
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
@ -71,16 +71,12 @@ describe('HttpClient', () => {
test('should handle timeout', async () => { test('should handle timeout', async () => {
const clientWithTimeout = new HttpClient({ timeout: 1 }); // 1ms timeout const clientWithTimeout = new HttpClient({ timeout: 1 }); // 1ms timeout
await expect( await expect(clientWithTimeout.get('https://httpbin.org/delay/1')).rejects.toThrow();
clientWithTimeout.get('https://httpbin.org/delay/1')
).rejects.toThrow();
}); });
}); });
describe('Error handling', () => { describe('Error handling', () => {
test('should handle HTTP errors', async () => { test('should handle HTTP errors', async () => {
await expect( await expect(client.get(`${mockServerBaseUrl}/status/404`)).rejects.toThrow(HttpError);
client.get(`${mockServerBaseUrl}/status/404`)
).rejects.toThrow(HttpError);
}); });
test('should handle network errors gracefully', async () => { test('should handle network errors gracefully', async () => {
@ -90,9 +86,7 @@ describe('HttpClient', () => {
}); });
test('should handle invalid URLs', async () => { test('should handle invalid URLs', async () => {
await expect( await expect(client.get('not:/a:valid/url')).rejects.toThrow();
client.get('not:/a:valid/url')
).rejects.toThrow();
}); });
}); });
@ -122,13 +116,13 @@ describe('ProxyManager', () => {
const httpProxy: ProxyInfo = { const httpProxy: ProxyInfo = {
protocol: 'http', protocol: 'http',
host: 'proxy.example.com', host: 'proxy.example.com',
port: 8080 port: 8080,
}; };
const socksProxy: ProxyInfo = { const socksProxy: ProxyInfo = {
protocol: 'socks5', protocol: 'socks5',
host: 'proxy.example.com', host: 'proxy.example.com',
port: 1080 port: 1080,
}; };
expect(ProxyManager.shouldUseBunFetch(httpProxy)).toBe(true); expect(ProxyManager.shouldUseBunFetch(httpProxy)).toBe(true);
@ -141,7 +135,8 @@ describe('ProxyManager', () => {
host: 'proxy.example.com', host: 'proxy.example.com',
port: 8080, port: 8080,
username: 'user', username: 'user',
password: 'pass' }; password: 'pass',
};
const proxyUrl = ProxyManager.createProxyUrl(proxy); const proxyUrl = ProxyManager.createProxyUrl(proxy);
expect(proxyUrl).toBe('http://user:pass@proxy.example.com:8080'); expect(proxyUrl).toBe('http://user:pass@proxy.example.com:8080');
@ -151,7 +146,8 @@ describe('ProxyManager', () => {
const proxy: ProxyInfo = { const proxy: ProxyInfo = {
protocol: 'https', protocol: 'https',
host: 'proxy.example.com', host: 'proxy.example.com',
port: 8080 }; port: 8080,
};
const proxyUrl = ProxyManager.createProxyUrl(proxy); const proxyUrl = ProxyManager.createProxyUrl(proxy);
expect(proxyUrl).toBe('https://proxy.example.com:8080'); expect(proxyUrl).toBe('https://proxy.example.com:8080');

View file

@ -1,4 +1,4 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { MockServer } from './mock-server'; import { MockServer } from './mock-server';
/** /**
@ -48,8 +48,9 @@ describe('MockServer', () => {
const response = await fetch(`${baseUrl}/headers`, { const response = await fetch(`${baseUrl}/headers`, {
headers: { headers: {
'X-Test-Header': 'test-value', 'X-Test-Header': 'test-value',
'User-Agent': 'MockServer-Test' 'User-Agent': 'MockServer-Test',
} }); },
});
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
const data = await response.json(); const data = await response.json();
@ -66,8 +67,8 @@ describe('MockServer', () => {
const response = await fetch(`${baseUrl}/basic-auth/${username}/${password}`, { const response = await fetch(`${baseUrl}/basic-auth/${username}/${password}`, {
headers: { headers: {
'Authorization': `Basic ${credentials}` Authorization: `Basic ${credentials}`,
} },
}); });
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
@ -81,8 +82,8 @@ describe('MockServer', () => {
const response = await fetch(`${baseUrl}/basic-auth/user/pass`, { const response = await fetch(`${baseUrl}/basic-auth/user/pass`, {
headers: { headers: {
'Authorization': `Basic ${credentials}` Authorization: `Basic ${credentials}`,
} },
}); });
expect(response.status).toBe(401); expect(response.status).toBe(401);
@ -98,15 +99,15 @@ describe('MockServer', () => {
test('should echo POST data', async () => { test('should echo POST data', async () => {
const testData = { const testData = {
message: 'Hello, MockServer!', message: 'Hello, MockServer!',
timestamp: Date.now() timestamp: Date.now(),
}; };
const response = await fetch(`${baseUrl}/post`, { const response = await fetch(`${baseUrl}/post`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify(testData) body: JSON.stringify(testData),
}); });
expect(response.ok).toBe(true); expect(response.ok).toBe(true);

View file

@ -65,7 +65,9 @@ export class MockServer {
const parts = path.split('/').filter(Boolean); const parts = path.split('/').filter(Boolean);
const expectedUsername = parts[1]; const expectedUsername = parts[1];
const expectedPassword = parts[2]; const expectedPassword = parts[2];
console.log(`Basic auth endpoint called: expected user=${expectedUsername}, pass=${expectedPassword}`); console.log(
`Basic auth endpoint called: expected user=${expectedUsername}, pass=${expectedPassword}`
);
const authHeader = req.headers.get('authorization'); const authHeader = req.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Basic ')) { if (!authHeader || !authHeader.startsWith('Basic ')) {
@ -80,7 +82,7 @@ export class MockServer {
if (username === expectedUsername && password === expectedPassword) { if (username === expectedUsername && password === expectedPassword) {
return Response.json({ return Response.json({
authenticated: true, authenticated: true,
user: username user: username,
}); });
} }
@ -93,7 +95,7 @@ export class MockServer {
return Response.json({ return Response.json({
data, data,
headers: Object.fromEntries([...req.headers.entries()]), headers: Object.fromEntries([...req.headers.entries()]),
method: req.method method: req.method,
}); });
} }
@ -101,7 +103,7 @@ export class MockServer {
return Response.json({ return Response.json({
url: req.url, url: req.url,
method: req.method, method: req.method,
headers: Object.fromEntries([...req.headers.entries()]) headers: Object.fromEntries([...req.headers.entries()]),
}); });
} }

View file

@ -5,11 +5,7 @@
*/ */
// Core logger classes and functions // Core logger classes and functions
export { export { Logger, getLogger, shutdownLoggers } from './logger';
Logger,
getLogger,
shutdownLoggers
} from './logger';
// Type definitions // Type definitions
export type { LogLevel, LogContext, LogMetadata } from './types'; export type { LogLevel, LogContext, LogMetadata } from './types';

View file

@ -10,7 +10,7 @@
import pino from 'pino'; import pino from 'pino';
import { loggingConfig, lokiConfig } from '@stock-bot/config'; import { loggingConfig, lokiConfig } from '@stock-bot/config';
import type { LogLevel, LogContext, LogMetadata } from './types'; import type { LogContext, LogLevel, LogMetadata } from './types';
// Simple cache for logger instances // Simple cache for logger instances
const loggerCache = new Map<string, pino.Logger>(); const loggerCache = new Map<string, pino.Logger>();
@ -35,7 +35,7 @@ function createTransports(serviceName: string): any {
ignore: 'pid,hostname,service,environment,version,childName', ignore: 'pid,hostname,service,environment,version,childName',
errorLikeObjectKeys: ['err', 'error'], errorLikeObjectKeys: ['err', 'error'],
errorProps: 'message,stack,name,code', errorProps: 'message,stack,name,code',
} },
}); });
} }
@ -46,8 +46,8 @@ function createTransports(serviceName: string): any {
level: loggingConfig.LOG_LEVEL, level: loggingConfig.LOG_LEVEL,
options: { options: {
destination: `${loggingConfig.LOG_FILE_PATH}/${serviceName}.log`, destination: `${loggingConfig.LOG_FILE_PATH}/${serviceName}.log`,
mkdir: true mkdir: true,
} },
}); });
} }
@ -60,10 +60,10 @@ function createTransports(serviceName: string): any {
host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`, host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`,
labels: { labels: {
service: serviceName, service: serviceName,
environment: lokiConfig.LOKI_ENVIRONMENT_LABEL environment: lokiConfig.LOKI_ENVIRONMENT_LABEL,
}, },
ignore: 'childName', ignore: 'childName',
} },
}); });
} }
@ -82,8 +82,8 @@ function getPinoLogger(serviceName: string): pino.Logger {
base: { base: {
service: serviceName, service: serviceName,
environment: loggingConfig.LOG_ENVIRONMENT, environment: loggingConfig.LOG_ENVIRONMENT,
version: loggingConfig.LOG_SERVICE_VERSION version: loggingConfig.LOG_SERVICE_VERSION,
} },
}; };
if (transport) { if (transport) {
@ -96,7 +96,6 @@ function getPinoLogger(serviceName: string): pino.Logger {
return loggerCache.get(serviceName)!; return loggerCache.get(serviceName)!;
} }
/** /**
* Simplified Logger class * Simplified Logger class
*/ */
@ -138,7 +137,7 @@ export class Logger {
this.log('warn', message, metadata); this.log('warn', message, metadata);
} }
error(message: string | object, metadata?: LogMetadata & { error?: any } | unknown): void { error(message: string | object, metadata?: (LogMetadata & { error?: any }) | unknown): void {
let data: any = {}; let data: any = {};
// Handle metadata parameter normalization // Handle metadata parameter normalization
@ -189,14 +188,14 @@ export class Logger {
message: error.message || error.toString(), message: error.message || error.toString(),
...(error.stack && { stack: error.stack }), ...(error.stack && { stack: error.stack }),
...(error.code && { code: error.code }), ...(error.code && { code: error.code }),
...(error.status && { status: error.status }) ...(error.status && { status: error.status }),
}; };
} }
// Handle primitives (string, number, etc.) // Handle primitives (string, number, etc.)
return { return {
name: 'UnknownError', name: 'UnknownError',
message: String(error) message: String(error),
}; };
} }
/** /**
@ -211,7 +210,7 @@ export class Logger {
const childBindings = { const childBindings = {
service: this.serviceName, service: this.serviceName,
childName: ' -> ' + serviceName, childName: ' -> ' + serviceName,
...(context || childLogger.context) ...(context || childLogger.context),
}; };
childLogger.pino = this.pino.child(childBindings); childLogger.pino = this.pino.child(childBindings);
@ -243,9 +242,9 @@ export function getLogger(serviceName: string, context?: LogContext): Logger {
*/ */
export async function shutdownLoggers(): Promise<void> { export async function shutdownLoggers(): Promise<void> {
const flushPromises = Array.from(loggerCache.values()).map(logger => { const flushPromises = Array.from(loggerCache.values()).map(logger => {
return new Promise<void>((resolve) => { return new Promise<void>(resolve => {
if (typeof logger.flush === 'function') { if (typeof logger.flush === 'function') {
logger.flush((err) => { logger.flush(err => {
if (err) { if (err) {
console.error('Logger flush error:', err); console.error('Logger flush error:', err);
} }

View file

@ -5,7 +5,7 @@
* child loggers, and advanced error scenarios. * child loggers, and advanced error scenarios.
*/ */
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { Logger, shutdownLoggers } from '../src'; import { Logger, shutdownLoggers } from '../src';
import { loggerTestHelpers } from './setup'; import { loggerTestHelpers } from './setup';
@ -16,7 +16,8 @@ describe('Advanced Logger Features', () => {
beforeEach(() => { beforeEach(() => {
testLoggerInstance = loggerTestHelpers.createTestLogger('advanced-features'); testLoggerInstance = loggerTestHelpers.createTestLogger('advanced-features');
logger = testLoggerInstance.logger; logger = testLoggerInstance.logger;
}); afterEach(async () => { });
afterEach(async () => {
testLoggerInstance.clearCapturedLogs(); testLoggerInstance.clearCapturedLogs();
// Clear any global logger cache // Clear any global logger cache
await shutdownLoggers(); await shutdownLoggers();
@ -27,7 +28,7 @@ describe('Advanced Logger Features', () => {
const complexMetadata = { const complexMetadata = {
user: { id: '123', name: 'John Doe' }, user: { id: '123', name: 'John Doe' },
session: { id: 'sess-456', timeout: 3600 }, session: { id: 'sess-456', timeout: 3600 },
request: { method: 'POST', path: '/api/test' } request: { method: 'POST', path: '/api/test' },
}; };
logger.info('Complex operation', complexMetadata); logger.info('Complex operation', complexMetadata);
@ -42,7 +43,7 @@ describe('Advanced Logger Features', () => {
it('should handle arrays in metadata', () => { it('should handle arrays in metadata', () => {
const arrayMetadata = { const arrayMetadata = {
tags: ['user', 'authentication', 'success'], tags: ['user', 'authentication', 'success'],
ids: [1, 2, 3, 4] ids: [1, 2, 3, 4],
}; };
logger.info('Array metadata test', arrayMetadata); logger.info('Array metadata test', arrayMetadata);
@ -58,7 +59,7 @@ describe('Advanced Logger Features', () => {
nullValue: null, nullValue: null,
undefinedValue: undefined, undefinedValue: undefined,
emptyString: '', emptyString: '',
zeroValue: 0 zeroValue: 0,
}; };
logger.info('Null metadata test', nullMetadata); logger.info('Null metadata test', nullMetadata);
@ -75,7 +76,7 @@ describe('Advanced Logger Features', () => {
it('should create child logger with additional context', () => { it('should create child logger with additional context', () => {
const childLogger = logger.child({ const childLogger = logger.child({
component: 'auth-service', component: 'auth-service',
version: '1.2.3' version: '1.2.3',
}); });
childLogger.info('Child logger message'); childLogger.info('Child logger message');
@ -105,7 +106,7 @@ describe('Advanced Logger Features', () => {
childLogger.info('Request processed', { childLogger.info('Request processed', {
requestId: 'req-789', requestId: 'req-789',
duration: 150 duration: 150,
}); });
const logs = testLoggerInstance.getCapturedLogs(); const logs = testLoggerInstance.getCapturedLogs();
@ -137,7 +138,7 @@ describe('Advanced Logger Features', () => {
logger.error('Multiple errors', { logger.error('Multiple errors', {
primaryError: error1, primaryError: error1,
secondaryError: error2, secondaryError: error2,
context: 'batch processing' context: 'batch processing',
}); });
const logs = testLoggerInstance.getCapturedLogs(); const logs = testLoggerInstance.getCapturedLogs();

View file

@ -4,8 +4,8 @@
* Tests for the core logger functionality and utilities. * Tests for the core logger functionality and utilities.
*/ */
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { Logger, getLogger, shutdownLoggers } from '../src'; import { getLogger, Logger, shutdownLoggers } from '../src';
import { loggerTestHelpers } from './setup'; import { loggerTestHelpers } from './setup';
describe('Basic Logger Tests', () => { describe('Basic Logger Tests', () => {
@ -60,7 +60,7 @@ describe('Basic Logger Tests', () => {
const metadata = { const metadata = {
userId: 'user123', userId: 'user123',
sessionId: 'session456', sessionId: 'session456',
requestId: 'req789' requestId: 'req789',
}; };
logger.info('Request processed', metadata); logger.info('Request processed', metadata);
@ -77,7 +77,7 @@ describe('Basic Logger Tests', () => {
it('should create child loggers with additional context', () => { it('should create child loggers with additional context', () => {
const childLogger = logger.child({ const childLogger = logger.child({
module: 'payment', module: 'payment',
version: '1.0.0' version: '1.0.0',
}); });
childLogger.info('Payment processed'); childLogger.info('Payment processed');
@ -113,7 +113,7 @@ describe('Basic Logger Tests', () => {
const errorLike = { const errorLike = {
name: 'ValidationError', name: 'ValidationError',
message: 'Invalid input', message: 'Invalid input',
code: 'VALIDATION_FAILED' code: 'VALIDATION_FAILED',
}; };
logger.error('Validation failed', { error: errorLike }); logger.error('Validation failed', { error: errorLike });

View file

@ -4,12 +4,8 @@
* Tests the core functionality of the simplified @stock-bot/logger package. * Tests the core functionality of the simplified @stock-bot/logger package.
*/ */
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { import { getLogger, Logger, shutdownLoggers } from '../src';
Logger,
getLogger,
shutdownLoggers
} from '../src';
import { loggerTestHelpers } from './setup'; import { loggerTestHelpers } from './setup';
describe('Logger Integration Tests', () => { describe('Logger Integration Tests', () => {
@ -82,7 +78,7 @@ describe('Logger Integration Tests', () => {
// Create a child logger with additional context // Create a child logger with additional context
const childLogger = logger.child({ const childLogger = logger.child({
transactionId: 'tx-789', transactionId: 'tx-789',
operation: 'payment' operation: 'payment',
}); });
// Log with child logger // Log with child logger
@ -137,7 +133,7 @@ describe('Logger Integration Tests', () => {
const errorLike = { const errorLike = {
name: 'CustomError', name: 'CustomError',
message: 'Custom error message', message: 'Custom error message',
code: 'ERR_CUSTOM' code: 'ERR_CUSTOM',
}; };
logger.error('Custom error occurred', { error: errorLike }); logger.error('Custom error occurred', { error: errorLike });
@ -163,7 +159,7 @@ describe('Logger Integration Tests', () => {
const metadata = { const metadata = {
requestId: 'req-123', requestId: 'req-123',
userId: 'user-456', userId: 'user-456',
operation: 'data-fetch' operation: 'data-fetch',
}; };
logger.info('Operation completed', metadata); logger.info('Operation completed', metadata);
@ -179,7 +175,7 @@ describe('Logger Integration Tests', () => {
const objectMessage = { const objectMessage = {
event: 'user_action', event: 'user_action',
action: 'login', action: 'login',
timestamp: Date.now() timestamp: Date.now(),
}; };
logger.info(objectMessage); logger.info(objectMessage);

View file

@ -5,8 +5,8 @@
* Provides utilities and mocks for testing logging operations. * Provides utilities and mocks for testing logging operations.
*/ */
import { Logger, LogMetadata, shutdownLoggers } from '../src';
import { afterAll, afterEach, beforeAll, beforeEach } from 'bun:test'; import { afterAll, afterEach, beforeAll, beforeEach } from 'bun:test';
import { Logger, LogMetadata, shutdownLoggers } from '../src';
// Store original console methods // Store original console methods
const originalConsole = { const originalConsole = {
@ -14,19 +14,17 @@ const originalConsole = {
info: console.info, info: console.info,
warn: console.warn, warn: console.warn,
error: console.error, error: console.error,
debug: console.debug debug: console.debug,
}; };
// Create a test logger helper // Create a test logger helper
export const loggerTestHelpers = { export const loggerTestHelpers = {
/** /**
* Mock Loki transport * Mock Loki transport
*/ */
mockLokiTransport: () => ({ mockLokiTransport: () => ({
on: () => {}, on: () => {},
write: () => {} write: () => {},
}), }),
/** /**
* Create a mock Hono context for middleware tests * Create a mock Hono context for middleware tests
@ -49,13 +47,13 @@ export const loggerTestHelpers = {
raw: { raw: {
url: `http://localhost${path}`, url: `http://localhost${path}`,
method, method,
headers: rawHeaders headers: rawHeaders,
}, },
query: {}, query: {},
param: () => undefined, param: () => undefined,
header: (name: string) => rawHeaders.get(name.toLowerCase()), header: (name: string) => rawHeaders.get(name.toLowerCase()),
headers: headerMap, headers: headerMap,
...options.req ...options.req,
}; };
// Create mock response // Create mock response
@ -64,9 +62,11 @@ export const loggerTestHelpers = {
statusText: 'OK', statusText: 'OK',
body: null, body: null,
headers: new Map(), headers: new Map(),
clone: function() { return { ...this, text: async () => JSON.stringify(this.body) }; }, clone: function () {
return { ...this, text: async () => JSON.stringify(this.body) };
},
text: async () => JSON.stringify(res.body), text: async () => JSON.stringify(res.body),
...options.res ...options.res,
}; };
// Create context with all required Hono methods // Create context with all required Hono methods
@ -79,10 +79,23 @@ export const loggerTestHelpers = {
return c; return c;
}, },
get: (key: string) => c[key], get: (key: string) => c[key],
set: (key: string, value: any) => { c[key] = value; return c; }, set: (key: string, value: any) => {
status: (code: number) => { c.res.status = code; return c; }, c[key] = value;
json: (body: any) => { c.res.body = body; return c; }, return c;
executionCtx: { waitUntil: (fn: Function) => { fn(); } } },
status: (code: number) => {
c.res.status = code;
return c;
},
json: (body: any) => {
c.res.body = body;
return c;
},
executionCtx: {
waitUntil: (fn: Function) => {
fn();
},
},
}; };
return c; return c;
@ -96,7 +109,7 @@ export const loggerTestHelpers = {
// Do nothing, simulate middleware completion // Do nothing, simulate middleware completion
return; return;
}; };
} },
}; };
// Setup environment before tests // Setup environment before tests

View file

@ -77,7 +77,7 @@ export class MongoDBAggregationBuilder {
*/ */
unwind(field: string, options?: any): this { unwind(field: string, options?: any): this {
this.pipeline.push({ this.pipeline.push({
$unwind: options ? { path: field, ...options } : field $unwind: options ? { path: field, ...options } : field,
}); });
return this; return this;
} }
@ -91,8 +91,8 @@ export class MongoDBAggregationBuilder {
from, from,
localField, localField,
foreignField, foreignField,
as as,
} },
}); });
return this; return this;
} }
@ -145,7 +145,7 @@ export class MongoDBAggregationBuilder {
if (timeframe) { if (timeframe) {
matchConditions.timestamp = { matchConditions.timestamp = {
$gte: timeframe.start, $gte: timeframe.start,
$lte: timeframe.end $lte: timeframe.end,
}; };
} }
@ -156,11 +156,11 @@ export class MongoDBAggregationBuilder {
return this.group({ return this.group({
_id: { _id: {
symbol: '$symbol', symbol: '$symbol',
sentiment: '$sentiment_label' sentiment: '$sentiment_label',
}, },
count: { $sum: 1 }, count: { $sum: 1 },
avgScore: { $avg: '$sentiment_score' }, avgScore: { $avg: '$sentiment_score' },
avgConfidence: { $avg: '$confidence' } avgConfidence: { $avg: '$confidence' },
}); });
} }
@ -179,7 +179,7 @@ export class MongoDBAggregationBuilder {
articleCount: { $sum: 1 }, articleCount: { $sum: 1 },
symbols: { $addToSet: '$symbols' }, symbols: { $addToSet: '$symbols' },
avgSentiment: { $avg: '$sentiment_score' }, avgSentiment: { $avg: '$sentiment_score' },
latestArticle: { $max: '$published_date' } latestArticle: { $max: '$published_date' },
}); });
} }
@ -196,12 +196,12 @@ export class MongoDBAggregationBuilder {
return this.group({ return this.group({
_id: { _id: {
cik: '$cik', cik: '$cik',
company: '$company_name' company: '$company_name',
}, },
filingCount: { $sum: 1 }, filingCount: { $sum: 1 },
filingTypes: { $addToSet: '$filing_type' }, filingTypes: { $addToSet: '$filing_type' },
latestFiling: { $max: '$filing_date' }, latestFiling: { $max: '$filing_date' },
symbols: { $addToSet: '$symbols' } symbols: { $addToSet: '$symbols' },
}); });
} }
@ -216,7 +216,7 @@ export class MongoDBAggregationBuilder {
count: { $sum: 1 }, count: { $sum: 1 },
avgSizeBytes: { $avg: '$size_bytes' }, avgSizeBytes: { $avg: '$size_bytes' },
oldestDocument: { $min: '$created_at' }, oldestDocument: { $min: '$created_at' },
newestDocument: { $max: '$created_at' } newestDocument: { $max: '$created_at' },
}); });
} }
@ -234,14 +234,14 @@ export class MongoDBAggregationBuilder {
hour: { $dateToString: { format: '%Y-%m-%d %H:00:00', date: `$${dateField}` } }, hour: { $dateToString: { format: '%Y-%m-%d %H:00:00', date: `$${dateField}` } },
day: { $dateToString: { format: '%Y-%m-%d', date: `$${dateField}` } }, day: { $dateToString: { format: '%Y-%m-%d', date: `$${dateField}` } },
week: { $dateToString: { format: '%Y-W%V', date: `$${dateField}` } }, week: { $dateToString: { format: '%Y-W%V', date: `$${dateField}` } },
month: { $dateToString: { format: '%Y-%m', date: `$${dateField}` } } month: { $dateToString: { format: '%Y-%m', date: `$${dateField}` } },
}; };
return this.group({ return this.group({
_id: dateFormat[interval], _id: dateFormat[interval],
count: { $sum: 1 }, count: { $sum: 1 },
firstDocument: { $min: `$${dateField}` }, firstDocument: { $min: `$${dateField}` },
lastDocument: { $max: `$${dateField}` } lastDocument: { $max: `$${dateField}` },
}).sort({ _id: 1 }); }).sort({ _id: 1 });
} }
} }

View file

@ -1,21 +1,29 @@
import { MongoClient, Db, Collection, MongoClientOptions, Document, WithId, OptionalUnlessRequiredId } from 'mongodb'; import {
Collection,
Db,
Document,
MongoClient,
MongoClientOptions,
OptionalUnlessRequiredId,
WithId,
} from 'mongodb';
import * as yup from 'yup';
import { mongodbConfig } from '@stock-bot/config'; import { mongodbConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import type {
MongoDBClientConfig,
MongoDBConnectionOptions,
CollectionNames,
DocumentBase,
SentimentData,
RawDocument,
NewsArticle,
SecFiling,
EarningsTranscript,
AnalystReport
} from './types';
import { MongoDBHealthMonitor } from './health'; import { MongoDBHealthMonitor } from './health';
import { schemaMap } from './schemas'; import { schemaMap } from './schemas';
import * as yup from 'yup'; import type {
AnalystReport,
CollectionNames,
DocumentBase,
EarningsTranscript,
MongoDBClientConfig,
MongoDBConnectionOptions,
NewsArticle,
RawDocument,
SecFiling,
SentimentData,
} from './types';
/** /**
* MongoDB Client for Stock Bot * MongoDB Client for Stock Bot
@ -32,16 +40,13 @@ export class MongoDBClient {
private readonly healthMonitor: MongoDBHealthMonitor; private readonly healthMonitor: MongoDBHealthMonitor;
private isConnected = false; private isConnected = false;
constructor( constructor(config?: Partial<MongoDBClientConfig>, options?: MongoDBConnectionOptions) {
config?: Partial<MongoDBClientConfig>,
options?: MongoDBConnectionOptions
) {
this.config = this.buildConfig(config); this.config = this.buildConfig(config);
this.options = { this.options = {
retryAttempts: 3, retryAttempts: 3,
retryDelay: 1000, retryDelay: 1000,
healthCheckInterval: 30000, healthCheckInterval: 30000,
...options ...options,
}; };
this.logger = getLogger('mongodb-client'); this.logger = getLogger('mongodb-client');
@ -63,7 +68,9 @@ export class MongoDBClient {
for (let attempt = 1; attempt <= this.options.retryAttempts!; attempt++) { for (let attempt = 1; attempt <= this.options.retryAttempts!; attempt++) {
try { try {
this.logger.info(`Connecting to MongoDB (attempt ${attempt}/${this.options.retryAttempts})...`); this.logger.info(
`Connecting to MongoDB (attempt ${attempt}/${this.options.retryAttempts})...`
);
this.client = new MongoClient(uri, clientOptions); this.client = new MongoClient(uri, clientOptions);
await this.client.connect(); await this.client.connect();
@ -95,7 +102,9 @@ export class MongoDBClient {
} }
} }
throw new Error(`Failed to connect to MongoDB after ${this.options.retryAttempts} attempts: ${lastError?.message}`); throw new Error(
`Failed to connect to MongoDB after ${this.options.retryAttempts} attempts: ${lastError?.message}`
);
} }
/** /**
@ -134,7 +143,8 @@ export class MongoDBClient {
*/ */
async insertOne<T extends DocumentBase>( async insertOne<T extends DocumentBase>(
collectionName: CollectionNames, collectionName: CollectionNames,
document: Omit<T, '_id' | 'created_at' | 'updated_at'> & Partial<Pick<T, 'created_at' | 'updated_at'>> document: Omit<T, '_id' | 'created_at' | 'updated_at'> &
Partial<Pick<T, 'created_at' | 'updated_at'>>
): Promise<T> { ): Promise<T> {
const collection = this.getCollection<T>(collectionName); const collection = this.getCollection<T>(collectionName);
@ -143,7 +153,7 @@ export class MongoDBClient {
const docWithTimestamps = { const docWithTimestamps = {
...document, ...document,
created_at: document.created_at || now, created_at: document.created_at || now,
updated_at: now updated_at: now,
} as T; // Validate document if schema exists } as T; // Validate document if schema exists
if (collectionName in schemaMap) { if (collectionName in schemaMap) {
try { try {
@ -155,7 +165,8 @@ export class MongoDBClient {
} }
throw error; throw error;
} }
}const result = await collection.insertOne(docWithTimestamps as OptionalUnlessRequiredId<T>); }
const result = await collection.insertOne(docWithTimestamps as OptionalUnlessRequiredId<T>);
return { ...docWithTimestamps, _id: result.insertedId } as T; return { ...docWithTimestamps, _id: result.insertedId } as T;
} }
@ -172,7 +183,7 @@ export class MongoDBClient {
// Add updated timestamp // Add updated timestamp
const updateWithTimestamp = { const updateWithTimestamp = {
...update, ...update,
updated_at: new Date() updated_at: new Date(),
}; };
const result = await collection.updateOne(filter, { $set: updateWithTimestamp }); const result = await collection.updateOne(filter, { $set: updateWithTimestamp });
@ -187,7 +198,7 @@ export class MongoDBClient {
options: any = {} options: any = {}
): Promise<T[]> { ): Promise<T[]> {
const collection = this.getCollection<T>(collectionName); const collection = this.getCollection<T>(collectionName);
return await collection.find(filter, options).toArray() as T[]; return (await collection.find(filter, options).toArray()) as T[];
} }
/** /**
@ -198,7 +209,7 @@ export class MongoDBClient {
filter: any filter: any
): Promise<T | null> { ): Promise<T | null> {
const collection = this.getCollection<T>(collectionName); const collection = this.getCollection<T>(collectionName);
return await collection.findOne(filter) as T | null; return (await collection.findOne(filter)) as T | null;
} }
/** /**
@ -215,10 +226,7 @@ export class MongoDBClient {
/** /**
* Count documents * Count documents
*/ */
async countDocuments( async countDocuments(collectionName: CollectionNames, filter: any = {}): Promise<number> {
collectionName: CollectionNames,
filter: any = {}
): Promise<number> {
const collection = this.getCollection(collectionName); const collection = this.getCollection(collectionName);
return await collection.countDocuments(filter); return await collection.countDocuments(filter);
} }
@ -233,36 +241,41 @@ export class MongoDBClient {
try { try {
// Sentiment data indexes // Sentiment data indexes
await this.db.collection('sentiment_data').createIndexes([ await this.db
.collection('sentiment_data')
.createIndexes([
{ key: { symbol: 1, timestamp: -1 } }, { key: { symbol: 1, timestamp: -1 } },
{ key: { sentiment_label: 1 } }, { key: { sentiment_label: 1 } },
{ key: { source_type: 1 } }, { key: { source_type: 1 } },
{ key: { created_at: -1 } } { key: { created_at: -1 } },
]); ]);
// News articles indexes // News articles indexes
await this.db.collection('news_articles').createIndexes([ await this.db
.collection('news_articles')
.createIndexes([
{ key: { symbols: 1, published_date: -1 } }, { key: { symbols: 1, published_date: -1 } },
{ key: { publication: 1 } }, { key: { publication: 1 } },
{ key: { categories: 1 } }, { key: { categories: 1 } },
{ key: { created_at: -1 } } { key: { created_at: -1 } },
]); ]);
// SEC filings indexes // SEC filings indexes
await this.db.collection('sec_filings').createIndexes([ await this.db
.collection('sec_filings')
.createIndexes([
{ key: { symbols: 1, filing_date: -1 } }, { key: { symbols: 1, filing_date: -1 } },
{ key: { filing_type: 1 } }, { key: { filing_type: 1 } },
{ key: { cik: 1 } }, { key: { cik: 1 } },
{ key: { created_at: -1 } } { key: { created_at: -1 } },
]); // Raw documents indexes ]); // Raw documents indexes
await this.db.collection('raw_documents').createIndex( await this.db.collection('raw_documents').createIndex({ content_hash: 1 }, { unique: true });
{ content_hash: 1 }, await this.db
{ unique: true } .collection('raw_documents')
); .createIndexes([
await this.db.collection('raw_documents').createIndexes([
{ key: { processing_status: 1 } }, { key: { processing_status: 1 } },
{ key: { document_type: 1 } }, { key: { document_type: 1 } },
{ key: { created_at: -1 } } { key: { created_at: -1 } },
]); ]);
this.logger.info('MongoDB indexes created successfully'); this.logger.info('MongoDB indexes created successfully');
@ -316,27 +329,27 @@ export class MongoDBClient {
maxPoolSize: mongodbConfig.MONGODB_MAX_POOL_SIZE, maxPoolSize: mongodbConfig.MONGODB_MAX_POOL_SIZE,
minPoolSize: mongodbConfig.MONGODB_MIN_POOL_SIZE, minPoolSize: mongodbConfig.MONGODB_MIN_POOL_SIZE,
maxIdleTime: mongodbConfig.MONGODB_MAX_IDLE_TIME, maxIdleTime: mongodbConfig.MONGODB_MAX_IDLE_TIME,
...config?.poolSettings ...config?.poolSettings,
}, },
timeouts: { timeouts: {
connectTimeout: mongodbConfig.MONGODB_CONNECT_TIMEOUT, connectTimeout: mongodbConfig.MONGODB_CONNECT_TIMEOUT,
socketTimeout: mongodbConfig.MONGODB_SOCKET_TIMEOUT, socketTimeout: mongodbConfig.MONGODB_SOCKET_TIMEOUT,
serverSelectionTimeout: mongodbConfig.MONGODB_SERVER_SELECTION_TIMEOUT, serverSelectionTimeout: mongodbConfig.MONGODB_SERVER_SELECTION_TIMEOUT,
...config?.timeouts ...config?.timeouts,
}, },
tls: { tls: {
enabled: mongodbConfig.MONGODB_TLS, enabled: mongodbConfig.MONGODB_TLS,
insecure: mongodbConfig.MONGODB_TLS_INSECURE, insecure: mongodbConfig.MONGODB_TLS_INSECURE,
caFile: mongodbConfig.MONGODB_TLS_CA_FILE, caFile: mongodbConfig.MONGODB_TLS_CA_FILE,
...config?.tls ...config?.tls,
}, },
options: { options: {
retryWrites: mongodbConfig.MONGODB_RETRY_WRITES, retryWrites: mongodbConfig.MONGODB_RETRY_WRITES,
journal: mongodbConfig.MONGODB_JOURNAL, journal: mongodbConfig.MONGODB_JOURNAL,
readPreference: mongodbConfig.MONGODB_READ_PREFERENCE as any, readPreference: mongodbConfig.MONGODB_READ_PREFERENCE as any,
writeConcern: mongodbConfig.MONGODB_WRITE_CONCERN, writeConcern: mongodbConfig.MONGODB_WRITE_CONCERN,
...config?.options ...config?.options,
} },
}; };
} }
@ -362,14 +375,18 @@ export class MongoDBClient {
serverSelectionTimeoutMS: this.config.timeouts?.serverSelectionTimeout, serverSelectionTimeoutMS: this.config.timeouts?.serverSelectionTimeout,
retryWrites: this.config.options?.retryWrites, retryWrites: this.config.options?.retryWrites,
journal: this.config.options?.journal, journal: this.config.options?.journal,
readPreference: this.config.options?.readPreference, writeConcern: this.config.options?.writeConcern ? { readPreference: this.config.options?.readPreference,
w: this.config.options.writeConcern === 'majority' writeConcern: this.config.options?.writeConcern
? 'majority' as const ? {
: parseInt(this.config.options.writeConcern, 10) || 1 w:
} : undefined, this.config.options.writeConcern === 'majority'
? ('majority' as const)
: parseInt(this.config.options.writeConcern, 10) || 1,
}
: undefined,
tls: this.config.tls?.enabled, tls: this.config.tls?.enabled,
tlsInsecure: this.config.tls?.insecure, tlsInsecure: this.config.tls?.insecure,
tlsCAFile: this.config.tls?.caFile tlsCAFile: this.config.tls?.caFile,
}; };
} }

View file

@ -1,5 +1,5 @@
import { MongoDBClient } from './client';
import { mongodbConfig } from '@stock-bot/config'; import { mongodbConfig } from '@stock-bot/config';
import { MongoDBClient } from './client';
import type { MongoDBClientConfig, MongoDBConnectionOptions } from './types'; import type { MongoDBClientConfig, MongoDBConnectionOptions } from './types';
/** /**
@ -22,7 +22,7 @@ export function createDefaultMongoDBClient(): MongoDBClient {
database: mongodbConfig.MONGODB_DATABASE, database: mongodbConfig.MONGODB_DATABASE,
username: mongodbConfig.MONGODB_USERNAME, username: mongodbConfig.MONGODB_USERNAME,
password: mongodbConfig.MONGODB_PASSWORD, password: mongodbConfig.MONGODB_PASSWORD,
uri: mongodbConfig.MONGODB_URI uri: mongodbConfig.MONGODB_URI,
}; };
return new MongoDBClient(config); return new MongoDBClient(config);

View file

@ -22,7 +22,7 @@ export class MongoDBHealthMonitor {
averageLatency: 0, averageLatency: 0,
errorRate: 0, errorRate: 0,
connectionPoolUtilization: 0, connectionPoolUtilization: 0,
documentsProcessed: 0 documentsProcessed: 0,
}; };
} }
@ -121,7 +121,6 @@ export class MongoDBHealthMonitor {
errors.push(`High latency: ${latency}ms`); errors.push(`High latency: ${latency}ms`);
status = status === 'healthy' ? 'degraded' : status; status = status === 'healthy' ? 'degraded' : status;
} }
} catch (statusError) { } catch (statusError) {
errors.push(`Failed to get server status: ${(statusError as Error).message}`); errors.push(`Failed to get server status: ${(statusError as Error).message}`);
status = 'degraded'; status = 'degraded';
@ -143,7 +142,7 @@ export class MongoDBHealthMonitor {
timestamp: new Date(), timestamp: new Date(),
latency, latency,
connections: connectionStats, connections: connectionStats,
errors: errors.length > 0 ? errors : undefined errors: errors.length > 0 ? errors : undefined,
}; };
// Log health status changes // Log health status changes
@ -164,7 +163,10 @@ export class MongoDBHealthMonitor {
const dur = serverStatus.dur || {}; const dur = serverStatus.dur || {};
// Calculate operations per second (approximate) // Calculate operations per second (approximate)
const totalOps = Object.values(opcounters).reduce((sum: number, count: any) => sum + (count || 0), 0); const totalOps = Object.values(opcounters).reduce(
(sum: number, count: any) => sum + (count || 0),
0
);
this.metrics.operationsPerSecond = totalOps; this.metrics.operationsPerSecond = totalOps;
// Connection pool utilization // Connection pool utilization
@ -176,7 +178,8 @@ export class MongoDBHealthMonitor {
// Average latency (from durability stats if available) // Average latency (from durability stats if available)
if (dur.timeMS) { if (dur.timeMS) {
this.metrics.averageLatency = dur.timeMS.dt || 0; this.metrics.averageLatency = dur.timeMS.dt || 0;
} } catch (error) { }
} catch (error) {
this.logger.debug('Error parsing server status for metrics:', error as any); this.logger.debug('Error parsing server status for metrics:', error as any);
} }
} }
@ -184,7 +187,11 @@ export class MongoDBHealthMonitor {
/** /**
* Get connection pool statistics * Get connection pool statistics
*/ */
private getConnectionPoolStats(serverStatus: any): { utilization: number; active: number; available: number } { private getConnectionPoolStats(serverStatus: any): {
utilization: number;
active: number;
available: number;
} {
const connections = serverStatus.connections || {}; const connections = serverStatus.connections || {};
const active = connections.current || 0; const active = connections.current || 0;
const available = connections.available || 0; const available = connections.available || 0;
@ -193,7 +200,7 @@ export class MongoDBHealthMonitor {
return { return {
utilization: total > 0 ? active / total : 0, utilization: total > 0 ? active / total : 0,
active, active,
available available,
}; };
} }
@ -206,7 +213,7 @@ export class MongoDBHealthMonitor {
return { return {
active: 1, active: 1,
available: 9, available: 9,
total: 10 total: 10,
}; };
} }

View file

@ -23,7 +23,7 @@ export type {
NewsArticle, NewsArticle,
SecFiling, SecFiling,
EarningsTranscript, EarningsTranscript,
AnalystReport AnalystReport,
} from './types'; } from './types';
// Schemas // Schemas
@ -33,7 +33,7 @@ export {
newsArticleSchema, newsArticleSchema,
secFilingSchema, secFilingSchema,
earningsTranscriptSchema, earningsTranscriptSchema,
analystReportSchema analystReportSchema,
} from './schemas'; } from './schemas';
// Utils // Utils

View file

@ -26,11 +26,15 @@ export const sentimentDataSchema = documentBaseSchema.shape({
processed_at: yup.date().required(), processed_at: yup.date().required(),
language: yup.string().default('en'), language: yup.string().default('en'),
keywords: yup.array(yup.string()).required(), keywords: yup.array(yup.string()).required(),
entities: yup.array(yup.object({ entities: yup
.array(
yup.object({
name: yup.string().required(), name: yup.string().required(),
type: yup.string().required(), type: yup.string().required(),
confidence: yup.number().min(0).max(1).required(), confidence: yup.number().min(0).max(1).required(),
})).required(), })
)
.required(),
}); });
// Raw Document Schema // Raw Document Schema
@ -77,10 +81,14 @@ export const secFilingSchema = documentBaseSchema.shape({
url: yup.string().url().required(), url: yup.string().url().required(),
content: yup.string().required(), content: yup.string().required(),
extracted_data: yup.object().optional(), extracted_data: yup.object().optional(),
financial_statements: yup.array(yup.object({ financial_statements: yup
.array(
yup.object({
statement_type: yup.string().required(), statement_type: yup.string().required(),
data: yup.object().required(), data: yup.object().required(),
})).optional(), })
)
.optional(),
processing_status: yup.string().oneOf(['pending', 'processed', 'failed']).required(), processing_status: yup.string().oneOf(['pending', 'processed', 'failed']).required(),
}); });
@ -92,16 +100,22 @@ export const earningsTranscriptSchema = documentBaseSchema.shape({
year: yup.number().min(2000).max(3000).required(), year: yup.number().min(2000).max(3000).required(),
call_date: yup.date().required(), call_date: yup.date().required(),
transcript: yup.string().required(), transcript: yup.string().required(),
participants: yup.array(yup.object({ participants: yup
.array(
yup.object({
name: yup.string().required(), name: yup.string().required(),
title: yup.string().required(), title: yup.string().required(),
type: yup.string().oneOf(['executive', 'analyst']).required(), type: yup.string().oneOf(['executive', 'analyst']).required(),
})).required(), })
)
.required(),
key_topics: yup.array(yup.string()).required(), key_topics: yup.array(yup.string()).required(),
sentiment_analysis: yup.object({ sentiment_analysis: yup
.object({
overall_sentiment: yup.number().min(-1).max(1).required(), overall_sentiment: yup.number().min(-1).max(1).required(),
topic_sentiments: yup.object().required(), topic_sentiments: yup.object().required(),
}).optional(), })
.optional(),
financial_highlights: yup.object().optional(), financial_highlights: yup.object().optional(),
}); });

View file

@ -1,7 +1,7 @@
import type { OptionalUnlessRequiredId, WithId } from 'mongodb';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import type { MongoDBClient } from './client'; import type { MongoDBClient } from './client';
import type { CollectionNames, DocumentBase } from './types'; import type { CollectionNames, DocumentBase } from './types';
import type { WithId, OptionalUnlessRequiredId } from 'mongodb';
/** /**
* MongoDB Transaction Manager * MongoDB Transaction Manager
@ -42,17 +42,17 @@ export class MongoDBTransactionManager {
const result = await session.withTransaction( const result = await session.withTransaction(
async () => { async () => {
return await operations(session); return await operations(session);
}, { },
{
readPreference: options?.readPreference as any, readPreference: options?.readPreference as any,
readConcern: { level: options?.readConcern || 'majority' } as any, readConcern: { level: options?.readConcern || 'majority' } as any,
writeConcern: options?.writeConcern || { w: 'majority' }, writeConcern: options?.writeConcern || { w: 'majority' },
maxCommitTimeMS: options?.maxCommitTimeMS || 10000 maxCommitTimeMS: options?.maxCommitTimeMS || 10000,
} }
); );
this.logger.debug('MongoDB transaction completed successfully'); this.logger.debug('MongoDB transaction completed successfully');
return result; return result;
} catch (error) { } catch (error) {
this.logger.error('MongoDB transaction failed:', error); this.logger.error('MongoDB transaction failed:', error);
throw error; throw error;
@ -71,7 +71,7 @@ export class MongoDBTransactionManager {
}>, }>,
options?: { ordered?: boolean; bypassDocumentValidation?: boolean } options?: { ordered?: boolean; bypassDocumentValidation?: boolean }
): Promise<void> { ): Promise<void> {
await this.withTransaction(async (session) => { await this.withTransaction(async session => {
for (const operation of operations) { for (const operation of operations) {
const collection = this.client.getCollection(operation.collection); const collection = this.client.getCollection(operation.collection);
@ -80,16 +80,18 @@ export class MongoDBTransactionManager {
const documentsWithTimestamps = operation.documents.map(doc => ({ const documentsWithTimestamps = operation.documents.map(doc => ({
...doc, ...doc,
created_at: doc.created_at || now, created_at: doc.created_at || now,
updated_at: now updated_at: now,
})); }));
await collection.insertMany(documentsWithTimestamps, { await collection.insertMany(documentsWithTimestamps, {
session, session,
ordered: options?.ordered ?? true, ordered: options?.ordered ?? true,
bypassDocumentValidation: options?.bypassDocumentValidation ?? false bypassDocumentValidation: options?.bypassDocumentValidation ?? false,
}); });
this.logger.debug(`Inserted ${documentsWithTimestamps.length} documents into ${operation.collection}`); this.logger.debug(
`Inserted ${documentsWithTimestamps.length} documents into ${operation.collection}`
);
} }
}); });
} }
@ -105,7 +107,7 @@ export class MongoDBTransactionManager {
options?: any; options?: any;
}> }>
): Promise<void> { ): Promise<void> {
await this.withTransaction(async (session) => { await this.withTransaction(async session => {
const results = []; const results = [];
for (const operation of operations) { for (const operation of operations) {
@ -116,18 +118,14 @@ export class MongoDBTransactionManager {
...operation.update, ...operation.update,
$set: { $set: {
...operation.update.$set, ...operation.update.$set,
updated_at: new Date() updated_at: new Date(),
} },
}; };
const result = await collection.updateMany( const result = await collection.updateMany(operation.filter, updateWithTimestamp, {
operation.filter,
updateWithTimestamp,
{
session, session,
...operation.options ...operation.options,
} });
);
results.push(result); results.push(result);
this.logger.debug(`Updated ${result.modifiedCount} documents in ${operation.collection}`); this.logger.debug(`Updated ${result.modifiedCount} documents in ${operation.collection}`);
@ -146,7 +144,7 @@ export class MongoDBTransactionManager {
filter: any, filter: any,
transform?: (doc: T) => T transform?: (doc: T) => T
): Promise<number> { ): Promise<number> {
return await this.withTransaction(async (session) => { return await this.withTransaction(async session => {
const sourceCollection = this.client.getCollection<T>(fromCollection); const sourceCollection = this.client.getCollection<T>(fromCollection);
const targetCollection = this.client.getCollection<T>(toCollection); const targetCollection = this.client.getCollection<T>(toCollection);
@ -165,12 +163,16 @@ export class MongoDBTransactionManager {
documentsToInsert.forEach(doc => { documentsToInsert.forEach(doc => {
doc.updated_at = now; doc.updated_at = now;
}); // Insert into target collection }); // Insert into target collection
await targetCollection.insertMany(documentsToInsert as OptionalUnlessRequiredId<T>[], { session }); await targetCollection.insertMany(documentsToInsert as OptionalUnlessRequiredId<T>[], {
session,
});
// Remove from source collection // Remove from source collection
const deleteResult = await sourceCollection.deleteMany(filter, { session }); const deleteResult = await sourceCollection.deleteMany(filter, { session });
this.logger.info(`Moved ${documents.length} documents from ${fromCollection} to ${toCollection}`); this.logger.info(
`Moved ${documents.length} documents from ${fromCollection} to ${toCollection}`
);
return deleteResult.deletedCount || 0; return deleteResult.deletedCount || 0;
}); });
@ -188,15 +190,14 @@ export class MongoDBTransactionManager {
let totalArchived = 0; let totalArchived = 0;
while (true) { while (true) {
const batchArchived = await this.withTransaction(async (session) => { const batchArchived = await this.withTransaction(async session => {
const collection = this.client.getCollection(sourceCollection); const collection = this.client.getCollection(sourceCollection);
const archiveCol = this.client.getCollection(archiveCollection); const archiveCol = this.client.getCollection(archiveCollection);
// Find old documents // Find old documents
const documents = await collection.find( const documents = await collection
{ created_at: { $lt: cutoffDate } }, .find({ created_at: { $lt: cutoffDate } }, { limit: batchSize, session })
{ limit: batchSize, session } .toArray();
).toArray();
if (documents.length === 0) { if (documents.length === 0) {
return 0; return 0;
@ -207,7 +208,7 @@ export class MongoDBTransactionManager {
const documentsToArchive = documents.map(doc => ({ const documentsToArchive = documents.map(doc => ({
...doc, ...doc,
archived_at: now, archived_at: now,
archived_from: sourceCollection archived_from: sourceCollection,
})); }));
// Insert into archive collection // Insert into archive collection
@ -215,10 +216,7 @@ export class MongoDBTransactionManager {
// Remove from source collection // Remove from source collection
const ids = documents.map(doc => doc._id); const ids = documents.map(doc => doc._id);
const deleteResult = await collection.deleteMany( const deleteResult = await collection.deleteMany({ _id: { $in: ids } }, { session });
{ _id: { $in: ids } },
{ session }
);
return deleteResult.deletedCount || 0; return deleteResult.deletedCount || 0;
}); });
@ -232,7 +230,9 @@ export class MongoDBTransactionManager {
this.logger.debug(`Archived batch of ${batchArchived} documents`); this.logger.debug(`Archived batch of ${batchArchived} documents`);
} }
this.logger.info(`Archived ${totalArchived} documents from ${sourceCollection} to ${archiveCollection}`); this.logger.info(
`Archived ${totalArchived} documents from ${sourceCollection} to ${archiveCollection}`
);
return totalArchived; return totalArchived;
} }
} }

View file

@ -1,5 +1,5 @@
import * as yup from 'yup';
import type { ObjectId } from 'mongodb'; import type { ObjectId } from 'mongodb';
import * as yup from 'yup';
/** /**
* MongoDB Client Configuration * MongoDB Client Configuration

View file

@ -1,15 +1,15 @@
import { Pool, PoolClient, QueryResult as PgQueryResult, QueryResultRow } from 'pg'; import { QueryResult as PgQueryResult, Pool, PoolClient, QueryResultRow } from 'pg';
import { postgresConfig } from '@stock-bot/config'; import { postgresConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { PostgreSQLHealthMonitor } from './health';
import { PostgreSQLQueryBuilder } from './query-builder';
import { PostgreSQLTransactionManager } from './transactions';
import type { import type {
PostgreSQLClientConfig, PostgreSQLClientConfig,
PostgreSQLConnectionOptions, PostgreSQLConnectionOptions,
QueryResult, QueryResult,
TransactionCallback TransactionCallback,
} from './types'; } from './types';
import { PostgreSQLHealthMonitor } from './health';
import { PostgreSQLQueryBuilder } from './query-builder';
import { PostgreSQLTransactionManager } from './transactions';
/** /**
* PostgreSQL Client for Stock Bot * PostgreSQL Client for Stock Bot
@ -26,16 +26,13 @@ export class PostgreSQLClient {
private readonly transactionManager: PostgreSQLTransactionManager; private readonly transactionManager: PostgreSQLTransactionManager;
private isConnected = false; private isConnected = false;
constructor( constructor(config?: Partial<PostgreSQLClientConfig>, options?: PostgreSQLConnectionOptions) {
config?: Partial<PostgreSQLClientConfig>,
options?: PostgreSQLConnectionOptions
) {
this.config = this.buildConfig(config); this.config = this.buildConfig(config);
this.options = { this.options = {
retryAttempts: 3, retryAttempts: 3,
retryDelay: 1000, retryDelay: 1000,
healthCheckInterval: 30000, healthCheckInterval: 30000,
...options ...options,
}; };
this.logger = getLogger('postgres-client'); this.logger = getLogger('postgres-client');
@ -55,7 +52,9 @@ export class PostgreSQLClient {
for (let attempt = 1; attempt <= this.options.retryAttempts!; attempt++) { for (let attempt = 1; attempt <= this.options.retryAttempts!; attempt++) {
try { try {
this.logger.info(`Connecting to PostgreSQL (attempt ${attempt}/${this.options.retryAttempts})...`); this.logger.info(
`Connecting to PostgreSQL (attempt ${attempt}/${this.options.retryAttempts})...`
);
this.pool = new Pool(this.buildPoolConfig()); this.pool = new Pool(this.buildPoolConfig());
@ -89,7 +88,9 @@ export class PostgreSQLClient {
} }
} }
throw new Error(`Failed to connect to PostgreSQL after ${this.options.retryAttempts} attempts: ${lastError?.message}`); throw new Error(
`Failed to connect to PostgreSQL after ${this.options.retryAttempts} attempts: ${lastError?.message}`
);
} }
/** /**
@ -115,7 +116,10 @@ export class PostgreSQLClient {
/** /**
* Execute a query * Execute a query
*/ */
async query<T extends QueryResultRow = any>(text: string, params?: any[]): Promise<QueryResult<T>> { async query<T extends QueryResultRow = any>(
text: string,
params?: any[]
): Promise<QueryResult<T>> {
if (!this.pool) { if (!this.pool) {
throw new Error('PostgreSQL client not connected'); throw new Error('PostgreSQL client not connected');
} }
@ -128,19 +132,19 @@ export class PostgreSQLClient {
this.logger.debug(`Query executed in ${executionTime}ms`, { this.logger.debug(`Query executed in ${executionTime}ms`, {
query: text.substring(0, 100), query: text.substring(0, 100),
params: params?.length params: params?.length,
}); });
return { return {
...result, ...result,
executionTime executionTime,
} as QueryResult<T>; } as QueryResult<T>;
} catch (error) { } catch (error) {
const executionTime = Date.now() - startTime; const executionTime = Date.now() - startTime;
this.logger.error(`Query failed after ${executionTime}ms:`, { this.logger.error(`Query failed after ${executionTime}ms:`, {
error, error,
query: text, query: text,
params params,
}); });
throw error; throw error;
} }
@ -191,7 +195,10 @@ export class PostgreSQLClient {
/** /**
* Execute a stored procedure or function * Execute a stored procedure or function
*/ */
async callFunction<T extends QueryResultRow = any>(functionName: string, params?: any[]): Promise<QueryResult<T>> { async callFunction<T extends QueryResultRow = any>(
functionName: string,
params?: any[]
): Promise<QueryResult<T>> {
const placeholders = params ? params.map((_, i) => `$${i + 1}`).join(', ') : ''; const placeholders = params ? params.map((_, i) => `$${i + 1}`).join(', ') : '';
const query = `SELECT * FROM ${functionName}(${placeholders})`; const query = `SELECT * FROM ${functionName}(${placeholders})`;
return await this.query<T>(query, params); return await this.query<T>(query, params);
@ -278,12 +285,12 @@ export class PostgreSQLClient {
min: postgresConfig.POSTGRES_POOL_MIN, min: postgresConfig.POSTGRES_POOL_MIN,
max: postgresConfig.POSTGRES_POOL_MAX, max: postgresConfig.POSTGRES_POOL_MAX,
idleTimeoutMillis: postgresConfig.POSTGRES_POOL_IDLE_TIMEOUT, idleTimeoutMillis: postgresConfig.POSTGRES_POOL_IDLE_TIMEOUT,
...config?.poolSettings ...config?.poolSettings,
}, },
ssl: { ssl: {
enabled: postgresConfig.POSTGRES_SSL, enabled: postgresConfig.POSTGRES_SSL,
rejectUnauthorized: postgresConfig.POSTGRES_SSL_REJECT_UNAUTHORIZED, rejectUnauthorized: postgresConfig.POSTGRES_SSL_REJECT_UNAUTHORIZED,
...config?.ssl ...config?.ssl,
}, },
timeouts: { timeouts: {
query: postgresConfig.POSTGRES_QUERY_TIMEOUT, query: postgresConfig.POSTGRES_QUERY_TIMEOUT,
@ -291,8 +298,8 @@ export class PostgreSQLClient {
statement: postgresConfig.POSTGRES_STATEMENT_TIMEOUT, statement: postgresConfig.POSTGRES_STATEMENT_TIMEOUT,
lock: postgresConfig.POSTGRES_LOCK_TIMEOUT, lock: postgresConfig.POSTGRES_LOCK_TIMEOUT,
idleInTransaction: postgresConfig.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT, idleInTransaction: postgresConfig.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
...config?.timeouts ...config?.timeouts,
} },
}; };
} }
@ -311,16 +318,18 @@ export class PostgreSQLClient {
statement_timeout: this.config.timeouts?.statement, statement_timeout: this.config.timeouts?.statement,
lock_timeout: this.config.timeouts?.lock, lock_timeout: this.config.timeouts?.lock,
idle_in_transaction_session_timeout: this.config.timeouts?.idleInTransaction, idle_in_transaction_session_timeout: this.config.timeouts?.idleInTransaction,
ssl: this.config.ssl?.enabled ? { ssl: this.config.ssl?.enabled
rejectUnauthorized: this.config.ssl.rejectUnauthorized ? {
} : false rejectUnauthorized: this.config.ssl.rejectUnauthorized,
}
: false,
}; };
} }
private setupErrorHandlers(): void { private setupErrorHandlers(): void {
if (!this.pool) return; if (!this.pool) return;
this.pool.on('error', (error) => { this.pool.on('error', error => {
this.logger.error('PostgreSQL pool error:', error); this.logger.error('PostgreSQL pool error:', error);
}); });

View file

@ -1,5 +1,5 @@
import { PostgreSQLClient } from './client';
import { postgresConfig } from '@stock-bot/config'; import { postgresConfig } from '@stock-bot/config';
import { PostgreSQLClient } from './client';
import type { PostgreSQLClientConfig, PostgreSQLConnectionOptions } from './types'; import type { PostgreSQLClientConfig, PostgreSQLConnectionOptions } from './types';
/** /**
@ -21,7 +21,7 @@ export function createDefaultPostgreSQLClient(): PostgreSQLClient {
port: postgresConfig.POSTGRES_PORT, port: postgresConfig.POSTGRES_PORT,
database: postgresConfig.POSTGRES_DATABASE, database: postgresConfig.POSTGRES_DATABASE,
username: postgresConfig.POSTGRES_USERNAME, username: postgresConfig.POSTGRES_USERNAME,
password: postgresConfig.POSTGRES_PASSWORD password: postgresConfig.POSTGRES_PASSWORD,
}; };
return new PostgreSQLClient(config); return new PostgreSQLClient(config);

Some files were not shown because too many files have changed in this diff Show more