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

View file

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

View file

@ -1,18 +1,18 @@
import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { MarketDataComponent } from './pages/market-data/market-data.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 { SettingsComponent } from './pages/settings/settings.component';
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'market-data', component: MarketDataComponent },
{ path: 'portfolio', component: PortfolioComponent },
{ path: 'strategies', component: StrategiesComponent },
{ path: 'risk-management', component: RiskManagementComponent },
{ path: 'settings', component: SettingsComponent },
{ path: '**', redirectTo: '/dashboard' }
];
import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { MarketDataComponent } from './pages/market-data/market-data.component';
import { PortfolioComponent } from './pages/portfolio/portfolio.component';
import { RiskManagementComponent } from './pages/risk-management/risk-management.component';
import { SettingsComponent } from './pages/settings/settings.component';
import { StrategiesComponent } from './pages/strategies/strategies.component';
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'market-data', component: MarketDataComponent },
{ path: 'portfolio', component: PortfolioComponent },
{ path: 'strategies', component: StrategiesComponent },
{ path: 'risk-management', component: RiskManagementComponent },
{ path: 'settings', component: SettingsComponent },
{ path: '**', redirectTo: '/dashboard' },
];

View file

@ -1,25 +1,25 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, trading-dashboard');
});
});
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, trading-dashboard');
});
});

View file

@ -1,40 +1,40 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
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';
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatChipsModule,
SidebarComponent,
NotificationsComponent
],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected title = 'Trading Dashboard';
protected sidenavOpened = signal(true);
toggleSidenav() {
this.sidenavOpened.set(!this.sidenavOpened());
}
onNavigationClick(route: string) {
// Handle navigation if needed
console.log('Navigating to:', route);
}
}
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 { MatToolbarModule } from '@angular/material/toolbar';
import { RouterOutlet } from '@angular/router';
import { NotificationsComponent } from './components/notifications/notifications';
import { SidebarComponent } from './components/sidebar/sidebar.component';
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatChipsModule,
SidebarComponent,
NotificationsComponent,
],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected title = 'Trading Dashboard';
protected sidenavOpened = signal(true);
toggleSidenav() {
this.sidenavOpened.set(!this.sidenavOpened());
}
onNavigationClick(route: string) {
// Handle navigation if needed
console.log('Navigating to:', route);
}
}

View file

@ -1,86 +1,94 @@
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatBadgeModule } from '@angular/material/badge';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';
import { MatDividerModule } from '@angular/material/divider';
import { NotificationService, Notification } from '../../services/notification.service';
@Component({
selector: 'app-notifications',
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatBadgeModule,
MatMenuModule,
MatListModule,
MatDividerModule
],
templateUrl: './notifications.html',
styleUrl: './notifications.css'
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
get notifications() {
return this.notificationService.notifications();
}
get unreadCount() {
return this.notificationService.unreadCount();
}
markAsRead(notification: Notification) {
this.notificationService.markAsRead(notification.id);
}
markAllAsRead() {
this.notificationService.markAllAsRead();
}
clearNotification(notification: Notification) {
this.notificationService.clearNotification(notification.id);
}
clearAll() {
this.notificationService.clearAllNotifications();
}
getNotificationIcon(type: string): string {
switch (type) {
case 'error': return 'error';
case 'warning': return 'warning';
case 'success': return 'check_circle';
case 'info':
default: return 'info';
}
}
getNotificationColor(type: string): string {
switch (type) {
case 'error': return 'text-red-600';
case 'warning': return 'text-yellow-600';
case 'success': return 'text-green-600';
case 'info':
default: return 'text-blue-600';
}
}
formatTime(timestamp: Date): string {
const now = new Date();
const diff = now.getTime() - timestamp.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
}
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
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({
selector: 'app-notifications',
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatBadgeModule,
MatMenuModule,
MatListModule,
MatDividerModule,
],
templateUrl: './notifications.html',
styleUrl: './notifications.css',
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
get notifications() {
return this.notificationService.notifications();
}
get unreadCount() {
return this.notificationService.unreadCount();
}
markAsRead(notification: Notification) {
this.notificationService.markAsRead(notification.id);
}
markAllAsRead() {
this.notificationService.markAllAsRead();
}
clearNotification(notification: Notification) {
this.notificationService.clearNotification(notification.id);
}
clearAll() {
this.notificationService.clearAllNotifications();
}
getNotificationIcon(type: string): string {
switch (type) {
case 'error':
return 'error';
case 'warning':
return 'warning';
case 'success':
return 'check_circle';
case 'info':
default:
return 'info';
}
}
getNotificationColor(type: string): string {
switch (type) {
case 'error':
return 'text-red-600';
case 'warning':
return 'text-yellow-600';
case 'success':
return 'text-green-600';
case 'info':
default:
return 'text-blue-600';
}
}
formatTime(timestamp: Date): string {
const now = new Date();
const diff = now.getTime() - timestamp.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
}

View file

@ -1,61 +1,56 @@
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
export interface NavigationItem {
label: string;
icon: string;
route: string;
active?: boolean;
}
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [
CommonModule,
MatSidenavModule,
MatButtonModule,
MatIconModule
],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css'
})
export class SidebarComponent {
opened = input<boolean>(true);
navigationItemClick = output<string>();
protected navigationItems: NavigationItem[] = [
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard', active: true },
{ label: 'Market Data', icon: 'trending_up', route: '/market-data' },
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
{ label: 'Strategies', icon: 'psychology', route: '/strategies' },
{ label: 'Risk Management', icon: 'security', route: '/risk-management' },
{ label: 'Settings', icon: 'settings', route: '/settings' }
];
constructor(private router: Router) {
// Listen to route changes to update active state
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.updateActiveRoute(event.urlAfterRedirects);
});
}
onNavigationClick(route: string) {
this.navigationItemClick.emit(route);
this.router.navigate([route]);
this.updateActiveRoute(route);
}
private updateActiveRoute(currentRoute: string) {
this.navigationItems.forEach(item => {
item.active = item.route === currentRoute;
});
}
}
import { CommonModule } from '@angular/common';
import { Component, input, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
export interface NavigationItem {
label: string;
icon: string;
route: string;
active?: boolean;
}
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [CommonModule, MatSidenavModule, MatButtonModule, MatIconModule],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css',
})
export class SidebarComponent {
opened = input<boolean>(true);
navigationItemClick = output<string>();
protected navigationItems: NavigationItem[] = [
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard', active: true },
{ label: 'Market Data', icon: 'trending_up', route: '/market-data' },
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
{ label: 'Strategies', icon: 'psychology', route: '/strategies' },
{ label: 'Risk Management', icon: 'security', route: '/risk-management' },
{ label: 'Settings', icon: 'settings', route: '/settings' },
];
constructor(private router: Router) {
// Listen to route changes to update active state
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.updateActiveRoute(event.urlAfterRedirects);
});
}
onNavigationClick(route: string) {
this.navigationItemClick.emit(route);
this.router.navigate([route]);
this.updateActiveRoute(route);
}
private updateActiveRoute(currentRoute: string) {
this.navigationItems.forEach(item => {
item.active = item.route === currentRoute;
});
}
}

View file

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

View file

@ -1,198 +1,205 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { ApiService } from '../../services/api.service';
import { WebSocketService } from '../../services/websocket.service';
import { interval, Subscription } from 'rxjs';
export interface ExtendedMarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
marketCap: string;
high52Week: number;
low52Week: number;
}
@Component({
selector: 'app-market-data',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatTabsModule,
MatProgressSpinnerModule,
MatSnackBarModule
],
templateUrl: './market-data.component.html',
styleUrl: './market-data.component.css'
})
export class MarketDataComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private webSocketService = inject(WebSocketService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected marketData = signal<ExtendedMarketData[]>([]);
protected currentTime = signal<string>(new Date().toLocaleTimeString());
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent', 'volume', 'marketCap'];
ngOnInit() {
// Update time every second
const timeSubscription = interval(1000).subscribe(() => {
this.currentTime.set(new Date().toLocaleTimeString());
});
this.subscriptions.push(timeSubscription);
// Load initial market data
this.loadMarketData();
// Subscribe to real-time market data updates
const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({
next: (update) => {
this.updateMarketData(update);
},
error: (err) => {
console.error('WebSocket market data error:', err);
}
});
this.subscriptions.push(wsSubscription);
// Fallback: Refresh market data every 30 seconds if WebSocket fails
const dataSubscription = interval(30000).subscribe(() => {
if (!this.webSocketService.isConnected()) {
this.loadMarketData();
}
});
this.subscriptions.push(dataSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadMarketData() {
this.apiService.getMarketData().subscribe({
next: (response) => {
// Convert MarketData to ExtendedMarketData with mock extended properties
const extendedData: ExtendedMarketData[] = response.data.map(item => ({
...item,
marketCap: this.getMockMarketCap(item.symbol),
high52Week: item.price * 1.3, // Mock 52-week high (30% above current)
low52Week: item.price * 0.7 // Mock 52-week low (30% below current)
}));
this.marketData.set(extendedData);
this.isLoading.set(false);
this.error.set(null);
},
error: (err) => {
console.error('Failed to load market data:', err);
this.error.set('Failed to load market data');
this.isLoading.set(false);
this.snackBar.open('Failed to load market data', 'Dismiss', { duration: 5000 });
// Use mock data as fallback
this.marketData.set(this.getMockData());
}
});
}
private getMockMarketCap(symbol: string): string {
const marketCaps: { [key: string]: string } = {
'AAPL': '2.98T',
'GOOGL': '1.78T',
'MSFT': '3.08T',
'TSLA': '789.2B',
'AMZN': '1.59T'
};
return marketCaps[symbol] || '1.00T';
}
private getMockData(): ExtendedMarketData[] {
return [
{
symbol: 'AAPL',
price: 192.53,
change: 2.41,
changePercent: 1.27,
volume: 45230000,
marketCap: '2.98T',
high52Week: 199.62,
low52Week: 164.08
},
{
symbol: 'GOOGL',
price: 2847.56,
change: -12.34,
changePercent: -0.43,
volume: 12450000,
marketCap: '1.78T',
high52Week: 3030.93,
low52Week: 2193.62
},
{
symbol: 'MSFT',
price: 415.26,
change: 8.73,
changePercent: 2.15,
volume: 23180000,
marketCap: '3.08T',
high52Week: 468.35,
low52Week: 309.45
},
{
symbol: 'TSLA',
price: 248.50,
change: -5.21,
changePercent: -2.05,
volume: 89760000,
marketCap: '789.2B',
high52Week: 299.29,
low52Week: 152.37
},
{
symbol: 'AMZN',
price: 152.74,
change: 3.18,
changePercent: 2.12,
volume: 34520000,
marketCap: '1.59T',
high52Week: 170.17,
low52Week: 118.35
}
];
}
refreshData() {
this.isLoading.set(true);
this.loadMarketData();
}
private updateMarketData(update: any) {
const currentData = this.marketData();
const updatedData = currentData.map(item => {
if (item.symbol === update.symbol) {
return {
...item,
price: update.price,
change: update.change,
changePercent: update.changePercent,
volume: update.volume
};
}
return item;
});
this.marketData.set(updatedData);
}
}
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 { 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 { MatTabsModule } from '@angular/material/tabs';
import { interval, Subscription } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { WebSocketService } from '../../services/websocket.service';
export interface ExtendedMarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
marketCap: string;
high52Week: number;
low52Week: number;
}
@Component({
selector: 'app-market-data',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatTabsModule,
MatProgressSpinnerModule,
MatSnackBarModule,
],
templateUrl: './market-data.component.html',
styleUrl: './market-data.component.css',
})
export class MarketDataComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private webSocketService = inject(WebSocketService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected marketData = signal<ExtendedMarketData[]>([]);
protected currentTime = signal<string>(new Date().toLocaleTimeString());
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns: string[] = [
'symbol',
'price',
'change',
'changePercent',
'volume',
'marketCap',
];
ngOnInit() {
// Update time every second
const timeSubscription = interval(1000).subscribe(() => {
this.currentTime.set(new Date().toLocaleTimeString());
});
this.subscriptions.push(timeSubscription);
// Load initial market data
this.loadMarketData();
// Subscribe to real-time market data updates
const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({
next: update => {
this.updateMarketData(update);
},
error: err => {
console.error('WebSocket market data error:', err);
},
});
this.subscriptions.push(wsSubscription);
// Fallback: Refresh market data every 30 seconds if WebSocket fails
const dataSubscription = interval(30000).subscribe(() => {
if (!this.webSocketService.isConnected()) {
this.loadMarketData();
}
});
this.subscriptions.push(dataSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadMarketData() {
this.apiService.getMarketData().subscribe({
next: response => {
// Convert MarketData to ExtendedMarketData with mock extended properties
const extendedData: ExtendedMarketData[] = response.data.map(item => ({
...item,
marketCap: this.getMockMarketCap(item.symbol),
high52Week: item.price * 1.3, // Mock 52-week high (30% above current)
low52Week: item.price * 0.7, // Mock 52-week low (30% below current)
}));
this.marketData.set(extendedData);
this.isLoading.set(false);
this.error.set(null);
},
error: err => {
console.error('Failed to load market data:', err);
this.error.set('Failed to load market data');
this.isLoading.set(false);
this.snackBar.open('Failed to load market data', 'Dismiss', { duration: 5000 });
// Use mock data as fallback
this.marketData.set(this.getMockData());
},
});
}
private getMockMarketCap(symbol: string): string {
const marketCaps: { [key: string]: string } = {
AAPL: '2.98T',
GOOGL: '1.78T',
MSFT: '3.08T',
TSLA: '789.2B',
AMZN: '1.59T',
};
return marketCaps[symbol] || '1.00T';
}
private getMockData(): ExtendedMarketData[] {
return [
{
symbol: 'AAPL',
price: 192.53,
change: 2.41,
changePercent: 1.27,
volume: 45230000,
marketCap: '2.98T',
high52Week: 199.62,
low52Week: 164.08,
},
{
symbol: 'GOOGL',
price: 2847.56,
change: -12.34,
changePercent: -0.43,
volume: 12450000,
marketCap: '1.78T',
high52Week: 3030.93,
low52Week: 2193.62,
},
{
symbol: 'MSFT',
price: 415.26,
change: 8.73,
changePercent: 2.15,
volume: 23180000,
marketCap: '3.08T',
high52Week: 468.35,
low52Week: 309.45,
},
{
symbol: 'TSLA',
price: 248.5,
change: -5.21,
changePercent: -2.05,
volume: 89760000,
marketCap: '789.2B',
high52Week: 299.29,
low52Week: 152.37,
},
{
symbol: 'AMZN',
price: 152.74,
change: 3.18,
changePercent: 2.12,
volume: 34520000,
marketCap: '1.59T',
high52Week: 170.17,
low52Week: 118.35,
},
];
}
refreshData() {
this.isLoading.set(true);
this.loadMarketData();
}
private updateMarketData(update: any) {
const currentData = this.marketData();
const updatedData = currentData.map(item => {
if (item.symbol === update.symbol) {
return {
...item,
price: update.price,
change: update.change,
changePercent: update.changePercent,
volume: update.volume,
};
}
return item;
});
this.marketData.set(updatedData);
}
}

View file

@ -1,159 +1,168 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
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 { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { ApiService } from '../../services/api.service';
import { interval, Subscription } from 'rxjs';
export interface Position {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice: number;
marketValue: number;
unrealizedPnL: number;
unrealizedPnLPercent: number;
dayChange: number;
dayChangePercent: number;
}
export interface PortfolioSummary {
totalValue: number;
totalCost: number;
totalPnL: number;
totalPnLPercent: number;
dayChange: number;
dayChangePercent: number;
cash: number;
positionsCount: number;
}
@Component({
selector: 'app-portfolio',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTabsModule
],
templateUrl: './portfolio.component.html',
styleUrl: './portfolio.component.css'
})
export class PortfolioComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected portfolioSummary = signal<PortfolioSummary>({
totalValue: 0,
totalCost: 0,
totalPnL: 0,
totalPnLPercent: 0,
dayChange: 0,
dayChangePercent: 0,
cash: 0,
positionsCount: 0
});
protected positions = signal<Position[]>([]);
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns = ['symbol', 'quantity', 'avgPrice', 'currentPrice', 'marketValue', 'unrealizedPnL', 'dayChange'];
ngOnInit() {
this.loadPortfolioData();
// Refresh portfolio data every 30 seconds
const portfolioSubscription = interval(30000).subscribe(() => {
this.loadPortfolioData();
});
this.subscriptions.push(portfolioSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadPortfolioData() {
// Since we don't have a portfolio endpoint yet, let's create mock data
// In a real implementation, this would call this.apiService.getPortfolio()
setTimeout(() => {
const mockPositions: Position[] = [
{
symbol: 'AAPL',
quantity: 100,
avgPrice: 180.50,
currentPrice: 192.53,
marketValue: 19253,
unrealizedPnL: 1203,
unrealizedPnLPercent: 6.67,
dayChange: 241,
dayChangePercent: 1.27
},
{
symbol: 'MSFT',
quantity: 50,
avgPrice: 400.00,
currentPrice: 415.26,
marketValue: 20763,
unrealizedPnL: 763,
unrealizedPnLPercent: 3.82,
dayChange: 436.50,
dayChangePercent: 2.15
},
{
symbol: 'GOOGL',
quantity: 10,
avgPrice: 2900.00,
currentPrice: 2847.56,
marketValue: 28475.60,
unrealizedPnL: -524.40,
unrealizedPnLPercent: -1.81,
dayChange: -123.40,
dayChangePercent: -0.43
}
];
const summary: PortfolioSummary = {
totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash
totalCost: mockPositions.reduce((sum, pos) => sum + (pos.avgPrice * pos.quantity), 0),
totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0),
totalPnLPercent: 0,
dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0),
dayChangePercent: 0,
cash: 25000,
positionsCount: mockPositions.length
};
summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100;
summary.dayChangePercent = (summary.dayChange / (summary.totalValue - summary.dayChange)) * 100;
this.positions.set(mockPositions);
this.portfolioSummary.set(summary);
this.isLoading.set(false);
this.error.set(null);
}, 1000);
}
refreshData() {
this.isLoading.set(true);
this.loadPortfolioData();
}
getPnLColor(value: number): string {
if (value > 0) return 'text-green-600';
if (value < 0) return 'text-red-600';
return 'text-gray-600';
}
}
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 { 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 { MatTabsModule } from '@angular/material/tabs';
import { interval, Subscription } from 'rxjs';
import { ApiService } from '../../services/api.service';
export interface Position {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice: number;
marketValue: number;
unrealizedPnL: number;
unrealizedPnLPercent: number;
dayChange: number;
dayChangePercent: number;
}
export interface PortfolioSummary {
totalValue: number;
totalCost: number;
totalPnL: number;
totalPnLPercent: number;
dayChange: number;
dayChangePercent: number;
cash: number;
positionsCount: number;
}
@Component({
selector: 'app-portfolio',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTabsModule,
],
templateUrl: './portfolio.component.html',
styleUrl: './portfolio.component.css',
})
export class PortfolioComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected portfolioSummary = signal<PortfolioSummary>({
totalValue: 0,
totalCost: 0,
totalPnL: 0,
totalPnLPercent: 0,
dayChange: 0,
dayChangePercent: 0,
cash: 0,
positionsCount: 0,
});
protected positions = signal<Position[]>([]);
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns = [
'symbol',
'quantity',
'avgPrice',
'currentPrice',
'marketValue',
'unrealizedPnL',
'dayChange',
];
ngOnInit() {
this.loadPortfolioData();
// Refresh portfolio data every 30 seconds
const portfolioSubscription = interval(30000).subscribe(() => {
this.loadPortfolioData();
});
this.subscriptions.push(portfolioSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadPortfolioData() {
// Since we don't have a portfolio endpoint yet, let's create mock data
// In a real implementation, this would call this.apiService.getPortfolio()
setTimeout(() => {
const mockPositions: Position[] = [
{
symbol: 'AAPL',
quantity: 100,
avgPrice: 180.5,
currentPrice: 192.53,
marketValue: 19253,
unrealizedPnL: 1203,
unrealizedPnLPercent: 6.67,
dayChange: 241,
dayChangePercent: 1.27,
},
{
symbol: 'MSFT',
quantity: 50,
avgPrice: 400.0,
currentPrice: 415.26,
marketValue: 20763,
unrealizedPnL: 763,
unrealizedPnLPercent: 3.82,
dayChange: 436.5,
dayChangePercent: 2.15,
},
{
symbol: 'GOOGL',
quantity: 10,
avgPrice: 2900.0,
currentPrice: 2847.56,
marketValue: 28475.6,
unrealizedPnL: -524.4,
unrealizedPnLPercent: -1.81,
dayChange: -123.4,
dayChangePercent: -0.43,
},
];
const summary: PortfolioSummary = {
totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash
totalCost: mockPositions.reduce((sum, pos) => sum + pos.avgPrice * pos.quantity, 0),
totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0),
totalPnLPercent: 0,
dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0),
dayChangePercent: 0,
cash: 25000,
positionsCount: mockPositions.length,
};
summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100;
summary.dayChangePercent =
(summary.dayChange / (summary.totalValue - summary.dayChange)) * 100;
this.positions.set(mockPositions);
this.portfolioSummary.set(summary);
this.isLoading.set(false);
this.error.set(null);
}, 1000);
}
refreshData() {
this.isLoading.set(true);
this.loadPortfolioData();
}
getPnLColor(value: number): string {
if (value > 0) return 'text-green-600';
if (value < 0) return 'text-red-600';
return 'text-gray-600';
}
}

View file

@ -1,135 +1,139 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ApiService, RiskThresholds, RiskEvaluation } from '../../services/api.service';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-risk-management',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatFormFieldModule,
MatInputModule,
MatSnackBarModule,
MatProgressSpinnerModule,
ReactiveFormsModule
],
templateUrl: './risk-management.component.html',
styleUrl: './risk-management.component.css'
})
export class RiskManagementComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private fb = inject(FormBuilder);
private subscriptions: Subscription[] = [];
protected riskThresholds = signal<RiskThresholds | null>(null);
protected riskHistory = signal<RiskEvaluation[]>([]);
protected isLoading = signal<boolean>(true);
protected isSaving = signal<boolean>(false);
protected error = signal<string | null>(null);
protected thresholdsForm: FormGroup;
protected displayedColumns = ['symbol', 'positionValue', 'riskLevel', 'violations', 'timestamp'];
constructor() {
this.thresholdsForm = this.fb.group({
maxPositionSize: [0, [Validators.required, Validators.min(0)]],
maxDailyLoss: [0, [Validators.required, Validators.min(0)]],
maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]]
});
}
ngOnInit() {
this.loadRiskThresholds();
this.loadRiskHistory();
// Refresh risk history every 30 seconds
const historySubscription = interval(30000).subscribe(() => {
this.loadRiskHistory();
});
this.subscriptions.push(historySubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadRiskThresholds() {
this.apiService.getRiskThresholds().subscribe({
next: (response) => {
this.riskThresholds.set(response.data);
this.thresholdsForm.patchValue(response.data);
this.isLoading.set(false);
this.error.set(null);
},
error: (err) => {
console.error('Failed to load risk thresholds:', err);
this.error.set('Failed to load risk thresholds');
this.isLoading.set(false);
this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 });
}
});
}
private loadRiskHistory() {
this.apiService.getRiskHistory().subscribe({
next: (response) => {
this.riskHistory.set(response.data);
},
error: (err) => {
console.error('Failed to load risk history:', err);
this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 });
}
});
}
saveThresholds() {
if (this.thresholdsForm.valid) {
this.isSaving.set(true);
const thresholds = this.thresholdsForm.value as RiskThresholds;
this.apiService.updateRiskThresholds(thresholds).subscribe({
next: (response) => {
this.riskThresholds.set(response.data);
this.isSaving.set(false);
this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 });
},
error: (err) => {
console.error('Failed to save risk thresholds:', err);
this.isSaving.set(false);
this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 });
}
});
}
}
refreshData() {
this.isLoading.set(true);
this.loadRiskThresholds();
this.loadRiskHistory();
}
getRiskLevelColor(level: string): string {
switch (level) {
case 'LOW': return 'text-green-600';
case 'MEDIUM': return 'text-yellow-600';
case 'HIGH': return 'text-red-600';
default: return 'text-gray-600';
}
}
}
import { CommonModule } from '@angular/common';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { interval, Subscription } from 'rxjs';
import { ApiService, RiskEvaluation, RiskThresholds } from '../../services/api.service';
@Component({
selector: 'app-risk-management',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatFormFieldModule,
MatInputModule,
MatSnackBarModule,
MatProgressSpinnerModule,
ReactiveFormsModule,
],
templateUrl: './risk-management.component.html',
styleUrl: './risk-management.component.css',
})
export class RiskManagementComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private fb = inject(FormBuilder);
private subscriptions: Subscription[] = [];
protected riskThresholds = signal<RiskThresholds | null>(null);
protected riskHistory = signal<RiskEvaluation[]>([]);
protected isLoading = signal<boolean>(true);
protected isSaving = signal<boolean>(false);
protected error = signal<string | null>(null);
protected thresholdsForm: FormGroup;
protected displayedColumns = ['symbol', 'positionValue', 'riskLevel', 'violations', 'timestamp'];
constructor() {
this.thresholdsForm = this.fb.group({
maxPositionSize: [0, [Validators.required, Validators.min(0)]],
maxDailyLoss: [0, [Validators.required, Validators.min(0)]],
maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
});
}
ngOnInit() {
this.loadRiskThresholds();
this.loadRiskHistory();
// Refresh risk history every 30 seconds
const historySubscription = interval(30000).subscribe(() => {
this.loadRiskHistory();
});
this.subscriptions.push(historySubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadRiskThresholds() {
this.apiService.getRiskThresholds().subscribe({
next: response => {
this.riskThresholds.set(response.data);
this.thresholdsForm.patchValue(response.data);
this.isLoading.set(false);
this.error.set(null);
},
error: err => {
console.error('Failed to load risk thresholds:', err);
this.error.set('Failed to load risk thresholds');
this.isLoading.set(false);
this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 });
},
});
}
private loadRiskHistory() {
this.apiService.getRiskHistory().subscribe({
next: response => {
this.riskHistory.set(response.data);
},
error: err => {
console.error('Failed to load risk history:', err);
this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 });
},
});
}
saveThresholds() {
if (this.thresholdsForm.valid) {
this.isSaving.set(true);
const thresholds = this.thresholdsForm.value as RiskThresholds;
this.apiService.updateRiskThresholds(thresholds).subscribe({
next: response => {
this.riskThresholds.set(response.data);
this.isSaving.set(false);
this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 });
},
error: err => {
console.error('Failed to save risk thresholds:', err);
this.isSaving.set(false);
this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 });
},
});
}
}
refreshData() {
this.isLoading.set(true);
this.loadRiskThresholds();
this.loadRiskHistory();
}
getRiskLevelColor(level: string): string {
switch (level) {
case 'LOW':
return 'text-green-600';
case 'MEDIUM':
return 'text-yellow-600';
case 'HIGH':
return 'text-red-600';
default:
return 'text-gray-600';
}
}
}

View file

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

View file

@ -1,165 +1,165 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-drawdown-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="drawdown-chart-container">
<canvas #drawdownChart></canvas>
</div>
`,
styles: `
.drawdown-chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
`
})
export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Calculate drawdown series from daily returns
const drawdownData = this.calculateDrawdownSeries(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: drawdownData.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Drawdown',
data: drawdownData.drawdowns,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.3,
borderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return (value * 100).toFixed(1) + '%';
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
},
min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += (context.parsed.y * 100).toFixed(2) + '%';
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateDrawdownSeries(result: BacktestResult): {
dates: Date[];
drawdowns: number[];
} {
const dates: Date[] = [];
const drawdowns: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate equity curve
let equity = 1;
const equityCurve: number[] = [];
for (const daily of sortedReturns) {
equity *= (1 + daily.return);
equityCurve.push(equity);
dates.push(new Date(daily.date));
}
// Calculate running maximum (high water mark)
let hwm = equityCurve[0];
for (let i = 0; i < equityCurve.length; i++) {
// Update high water mark
hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark
const drawdown = (equityCurve[i] / hwm) - 1;
drawdowns.push(drawdown);
}
return { dates, drawdowns };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Chart, ChartOptions } from 'chart.js/auto';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-drawdown-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="drawdown-chart-container">
<canvas #drawdownChart></canvas>
</div>
`,
styles: `
.drawdown-chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
`,
})
export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Calculate drawdown series from daily returns
const drawdownData = this.calculateDrawdownSeries(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: drawdownData.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Drawdown',
data: drawdownData.drawdowns,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.3,
borderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0,
},
grid: {
display: false,
},
},
y: {
ticks: {
callback: function (value) {
return (value * 100).toFixed(1) + '%';
},
},
grid: {
color: 'rgba(200, 200, 200, 0.2)',
},
min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01,
},
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += (context.parsed.y * 100).toFixed(2) + '%';
}
return label;
},
},
},
legend: {
position: 'top',
},
},
} as ChartOptions,
});
}
private calculateDrawdownSeries(result: BacktestResult): {
dates: Date[];
drawdowns: number[];
} {
const dates: Date[] = [];
const drawdowns: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate equity curve
let equity = 1;
const equityCurve: number[] = [];
for (const daily of sortedReturns) {
equity *= 1 + daily.return;
equityCurve.push(equity);
dates.push(new Date(daily.date));
}
// Calculate running maximum (high water mark)
let hwm = equityCurve[0];
for (let i = 0; i < equityCurve.length; i++) {
// Update high water mark
hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark
const drawdown = equityCurve[i] / hwm - 1;
drawdowns.push(drawdown);
}
return { dates, drawdowns };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
}

View file

@ -1,171 +1,173 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-equity-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="equity-chart-container">
<canvas #equityChart></canvas>
</div>
`,
styles: `
.equity-chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
`
})
export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Prepare data
const equityCurve = this.calculateEquityCurve(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: equityCurve.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Portfolio Value',
data: equityCurve.values,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3,
borderWidth: 2,
fill: true
},
{
label: 'Benchmark',
data: equityCurve.benchmark,
borderColor: 'rgba(153, 102, 255, 0.5)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 1,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
}
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
.format(context.parsed.y);
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateEquityCurve(result: BacktestResult): {
dates: Date[];
values: number[];
benchmark: number[];
} {
const initialValue = result.initialCapital;
const dates: Date[] = [];
const values: number[] = [];
const benchmark: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate cumulative portfolio values
let portfolioValue = initialValue;
let benchmarkValue = initialValue;
for (const daily of sortedReturns) {
const date = new Date(daily.date);
portfolioValue = portfolioValue * (1 + daily.return);
// Simple benchmark (e.g., assuming 8% annualized return for a market index)
benchmarkValue = benchmarkValue * (1 + 0.08 / 365);
dates.push(date);
values.push(portfolioValue);
benchmark.push(benchmarkValue);
}
return { dates, values, benchmark };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Chart, ChartOptions } from 'chart.js/auto';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-equity-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="equity-chart-container">
<canvas #equityChart></canvas>
</div>
`,
styles: `
.equity-chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
`,
})
export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Prepare data
const equityCurve = this.calculateEquityCurve(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: equityCurve.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Portfolio Value',
data: equityCurve.values,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3,
borderWidth: 2,
fill: true,
},
{
label: 'Benchmark',
data: equityCurve.benchmark,
borderColor: 'rgba(153, 102, 255, 0.5)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 1,
fill: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0,
},
grid: {
display: false,
},
},
y: {
ticks: {
callback: function (value) {
return '$' + value.toLocaleString();
},
},
grid: {
color: 'rgba(200, 200, 200, 0.2)',
},
},
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(context.parsed.y);
}
return label;
},
},
},
legend: {
position: 'top',
},
},
} as ChartOptions,
});
}
private calculateEquityCurve(result: BacktestResult): {
dates: Date[];
values: number[];
benchmark: number[];
} {
const initialValue = result.initialCapital;
const dates: Date[] = [];
const values: number[] = [];
const benchmark: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate cumulative portfolio values
let portfolioValue = initialValue;
let benchmarkValue = initialValue;
for (const daily of sortedReturns) {
const date = new Date(daily.date);
portfolioValue = portfolioValue * (1 + daily.return);
// Simple benchmark (e.g., assuming 8% annualized return for a market index)
benchmarkValue = benchmarkValue * (1 + 0.08 / 365);
dates.push(date);
values.push(portfolioValue);
benchmark.push(benchmarkValue);
}
return { dates, values, benchmark };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
}

View file

@ -1,258 +1,304 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatDividerModule } from '@angular/material/divider';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatGridListModule,
MatDividerModule,
MatTooltipModule
],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">Total Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.totalReturn || 0)">
{{formatPercent(backtestResult?.totalReturn || 0)}}
</div>
</div>
<div class="metric">
<div 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)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{formatPercent(backtestResult?.cagr || 0)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">Max Drawdown</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.maxDrawdown || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">Max DD Duration</div>
<div class="metric-value">
{{formatDays(backtestResult?.maxDrawdownDuration || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">Volatility</div>
<div class="metric-value">
{{formatPercent(backtestResult?.volatility || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Square root of the sum of the squares of drawdowns">Ulcer Index</div>
<div class="metric-value">
{{(backtestResult?.ulcerIndex || 0).toFixed(4)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">Sharpe Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)">
{{(backtestResult?.sharpeRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">Sortino Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)">
{{(backtestResult?.sortinoRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">Calmar Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)">
{{(backtestResult?.calmarRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div 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)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{backtestResult?.totalTrades || 0}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{formatPercent(backtestResult?.winRate || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{formatPercent(backtestResult?.averageWinningTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.averageLosingTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">Profit Factor</div>
<div class="metric-value" [ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)">
{{(backtestResult?.profitFactor || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4CAF50;
}
.negative {
color: #F44336;
}
.neutral {
color: #FFA000;
}
mat-divider {
margin: 8px 0;
}
`
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) return 'positive';
if (value < 0) return 'negative';
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
if (value < 0) return 'negative';
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) return 'positive';
if (value >= 0.45) return 'neutral';
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
return 'negative';
}
}
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [CommonModule, MatCardModule, MatGridListModule, MatDividerModule, MatTooltipModule],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">
Total Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.totalReturn || 0)"
>
{{ formatPercent(backtestResult?.totalReturn || 0) }}
</div>
</div>
<div class="metric">
<div
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) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{ formatPercent(backtestResult?.cagr || 0) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">
Max Drawdown
</div>
<div class="metric-value negative">
{{ formatPercent(backtestResult?.maxDrawdown || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">
Max DD Duration
</div>
<div class="metric-value">
{{ formatDays(backtestResult?.maxDrawdownDuration || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">
Volatility
</div>
<div class="metric-value">
{{ formatPercent(backtestResult?.volatility || 0) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Square root of the sum of the squares of drawdowns"
>
Ulcer Index
</div>
<div class="metric-value">
{{ (backtestResult?.ulcerIndex || 0).toFixed(4) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">
Sharpe Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)"
>
{{ (backtestResult?.sharpeRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">
Sortino Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)"
>
{{ (backtestResult?.sortinoRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">
Calmar Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)"
>
{{ (backtestResult?.calmarRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div
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) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{ backtestResult?.totalTrades || 0 }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{ formatPercent(backtestResult?.winRate || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{ formatPercent(backtestResult?.averageWinningTrade || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{ formatPercent(backtestResult?.averageLosingTrade || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">
Profit Factor
</div>
<div
class="metric-value"
[ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)"
>
{{ (backtestResult?.profitFactor || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4caf50;
}
.negative {
color: #f44336;
}
.neutral {
color: #ffa000;
}
mat-divider {
margin: 8px 0;
}
`,
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) return 'positive';
if (value < 0) return 'negative';
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
if (value < 0) return 'negative';
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) return 'positive';
if (value >= 0.45) return 'neutral';
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
return 'negative';
}
}

View file

@ -1,221 +1,259 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-trades-table',
standalone: true,
imports: [
CommonModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatCardModule,
MatIconModule
],
template: `
<mat-card class="trades-card">
<mat-card-header>
<mat-card-title>Trades</mat-card-title>
</mat-card-header>
<mat-card-content>
<table mat-table [dataSource]="displayedTrades" matSort (matSortChange)="sortData($event)" class="trades-table">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </th>
<td mat-cell *matCellDef="let trade"> {{trade.symbol}} </td>
</ng-container>
<!-- Entry Date Column -->
<ng-container matColumnDef="entryTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Entry Time </th>
<td mat-cell *matCellDef="let trade"> {{formatDate(trade.entryTime)}} </td>
</ng-container>
<!-- Entry Price Column -->
<ng-container matColumnDef="entryPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Entry Price </th>
<td mat-cell *matCellDef="let trade"> {{formatCurrency(trade.entryPrice)}} </td>
</ng-container>
<!-- Exit Date Column -->
<ng-container matColumnDef="exitTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Exit Time </th>
<td mat-cell *matCellDef="let trade"> {{formatDate(trade.exitTime)}} </td>
</ng-container>
<!-- Exit Price Column -->
<ng-container matColumnDef="exitPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Exit Price </th>
<td mat-cell *matCellDef="let trade"> {{formatCurrency(trade.exitPrice)}} </td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Quantity </th>
<td mat-cell *matCellDef="let trade"> {{trade.quantity}} </td>
</ng-container>
<!-- P&L Column -->
<ng-container matColumnDef="pnl">
<th mat-header-cell *matHeaderCellDef mat-sort-header> P&L </th>
<td mat-cell *matCellDef="let trade"
[ngClass]="{'positive': trade.pnl > 0, 'negative': trade.pnl < 0}">
{{formatCurrency(trade.pnl)}}
</td>
</ng-container>
<!-- P&L Percent Column -->
<ng-container matColumnDef="pnlPercent">
<th mat-header-cell *matHeaderCellDef mat-sort-header> P&L % </th>
<td mat-cell *matCellDef="let trade"
[ngClass]="{'positive': trade.pnlPercent > 0, 'negative': trade.pnlPercent < 0}">
{{formatPercent(trade.pnlPercent)}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalTrades"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50]"
(page)="pageChange($event)"
aria-label="Select page">
</mat-paginator>
</mat-card-content>
</mat-card>
`,
styles: `
.trades-card {
margin-bottom: 20px;
}
.trades-table {
width: 100%;
border-collapse: collapse;
}
.mat-column-pnl, .mat-column-pnlPercent {
text-align: right;
font-weight: 500;
}
.positive {
color: #4CAF50;
}
.negative {
color: #F44336;
}
.mat-mdc-row:hover {
background-color: rgba(0, 0, 0, 0.04);
}
`
})
export class TradesTableComponent {
@Input() set backtestResult(value: BacktestResult | undefined) {
if (value) {
this._backtestResult = value;
this.updateDisplayedTrades();
}
}
get backtestResult(): BacktestResult | undefined {
return this._backtestResult;
}
private _backtestResult?: BacktestResult;
// Table configuration
displayedColumns: string[] = [
'symbol', 'entryTime', 'entryPrice', 'exitTime',
'exitPrice', 'quantity', 'pnl', 'pnlPercent'
];
// Pagination
pageSize = 10;
currentPage = 0;
displayedTrades: any[] = [];
get totalTrades(): number {
return this._backtestResult?.trades.length || 0;
}
// Sort the trades
sortData(sort: Sort): void {
if (!sort.active || sort.direction === '') {
this.updateDisplayedTrades();
return;
}
const data = this._backtestResult?.trades.slice() || [];
this.displayedTrades = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'symbol': return this.compare(a.symbol, b.symbol, isAsc);
case 'entryTime': return this.compare(new Date(a.entryTime).getTime(), new Date(b.entryTime).getTime(), isAsc);
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);
}
// Handle page changes
pageChange(event: PageEvent): void {
this.pageSize = event.pageSize;
this.currentPage = event.pageIndex;
this.updateDisplayedTrades();
}
// Update displayed trades based on current page and page size
updateDisplayedTrades(): void {
if (this._backtestResult) {
this.displayedTrades = this._backtestResult.trades.slice(
this.currentPage * this.pageSize,
(this.currentPage + 1) * this.pageSize
);
} else {
this.displayedTrades = [];
}
}
// Helper methods for formatting
formatDate(date: Date | string): string {
return new Date(date).toLocaleString();
}
formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
private compare(a: number | string, b: number | string, isAsc: boolean): number {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
}
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
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';
@Component({
selector: 'app-trades-table',
standalone: true,
imports: [
CommonModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatCardModule,
MatIconModule,
],
template: `
<mat-card class="trades-card">
<mat-card-header>
<mat-card-title>Trades</mat-card-title>
</mat-card-header>
<mat-card-content>
<table
mat-table
[dataSource]="displayedTrades"
matSort
(matSortChange)="sortData($event)"
class="trades-table"
>
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Symbol</th>
<td mat-cell *matCellDef="let trade">{{ trade.symbol }}</td>
</ng-container>
<!-- Entry Date Column -->
<ng-container matColumnDef="entryTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Entry Time</th>
<td mat-cell *matCellDef="let trade">{{ formatDate(trade.entryTime) }}</td>
</ng-container>
<!-- Entry Price Column -->
<ng-container matColumnDef="entryPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Entry Price</th>
<td mat-cell *matCellDef="let trade">{{ formatCurrency(trade.entryPrice) }}</td>
</ng-container>
<!-- Exit Date Column -->
<ng-container matColumnDef="exitTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Exit Time</th>
<td mat-cell *matCellDef="let trade">{{ formatDate(trade.exitTime) }}</td>
</ng-container>
<!-- Exit Price Column -->
<ng-container matColumnDef="exitPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Exit Price</th>
<td mat-cell *matCellDef="let trade">{{ formatCurrency(trade.exitPrice) }}</td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Quantity</th>
<td mat-cell *matCellDef="let trade">{{ trade.quantity }}</td>
</ng-container>
<!-- P&L Column -->
<ng-container matColumnDef="pnl">
<th mat-header-cell *matHeaderCellDef mat-sort-header>P&L</th>
<td
mat-cell
*matCellDef="let trade"
[ngClass]="{ positive: trade.pnl > 0, negative: trade.pnl < 0 }"
>
{{ formatCurrency(trade.pnl) }}
</td>
</ng-container>
<!-- P&L Percent Column -->
<ng-container matColumnDef="pnlPercent">
<th mat-header-cell *matHeaderCellDef mat-sort-header>P&L %</th>
<td
mat-cell
*matCellDef="let trade"
[ngClass]="{ positive: trade.pnlPercent > 0, negative: trade.pnlPercent < 0 }"
>
{{ formatPercent(trade.pnlPercent) }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
[length]="totalTrades"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50]"
(page)="pageChange($event)"
aria-label="Select page"
>
</mat-paginator>
</mat-card-content>
</mat-card>
`,
styles: `
.trades-card {
margin-bottom: 20px;
}
.trades-table {
width: 100%;
border-collapse: collapse;
}
.mat-column-pnl,
.mat-column-pnlPercent {
text-align: right;
font-weight: 500;
}
.positive {
color: #4caf50;
}
.negative {
color: #f44336;
}
.mat-mdc-row:hover {
background-color: rgba(0, 0, 0, 0.04);
}
`,
})
export class TradesTableComponent {
@Input() set backtestResult(value: BacktestResult | undefined) {
if (value) {
this._backtestResult = value;
this.updateDisplayedTrades();
}
}
get backtestResult(): BacktestResult | undefined {
return this._backtestResult;
}
private _backtestResult?: BacktestResult;
// Table configuration
displayedColumns: string[] = [
'symbol',
'entryTime',
'entryPrice',
'exitTime',
'exitPrice',
'quantity',
'pnl',
'pnlPercent',
];
// Pagination
pageSize = 10;
currentPage = 0;
displayedTrades: any[] = [];
get totalTrades(): number {
return this._backtestResult?.trades.length || 0;
}
// Sort the trades
sortData(sort: Sort): void {
if (!sort.active || sort.direction === '') {
this.updateDisplayedTrades();
return;
}
const data = this._backtestResult?.trades.slice() || [];
this.displayedTrades = data
.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'symbol':
return this.compare(a.symbol, b.symbol, isAsc);
case 'entryTime':
return this.compare(
new Date(a.entryTime).getTime(),
new Date(b.entryTime).getTime(),
isAsc
);
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);
}
// Handle page changes
pageChange(event: PageEvent): void {
this.pageSize = event.pageSize;
this.currentPage = event.pageIndex;
this.updateDisplayedTrades();
}
// Update displayed trades based on current page and page size
updateDisplayedTrades(): void {
if (this._backtestResult) {
this.displayedTrades = this._backtestResult.trades.slice(
this.currentPage * this.pageSize,
(this.currentPage + 1) * this.pageSize
);
} else {
this.displayedTrades = [];
}
}
// Helper methods for formatting
formatDate(date: Date | string): string {
return new Date(date).toLocaleString();
}
formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
private compare(a: number | string, b: number | string, isAsc: boolean): number {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
}

View file

@ -1,185 +1,193 @@
import { Component, Inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
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 { MatIconModule } from '@angular/material/icon';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import {
BacktestRequest,
BacktestResult,
StrategyService,
TradingStrategy
} from '../../../services/strategy.service';
@Component({
selector: 'app-backtest-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
MatNativeDateModule,
MatProgressBarModule,
MatTabsModule,
MatChipsModule,
MatIconModule,
MatSlideToggleModule
],
templateUrl: './backtest-dialog.component.html',
styleUrl: './backtest-dialog.component.css'
})
export class BacktestDialogComponent implements OnInit {
backtestForm: FormGroup;
strategyTypes: string[] = [];
availableSymbols: string[] = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'SPY', 'QQQ'];
selectedSymbols: string[] = [];
parameters: Record<string, any> = {};
isRunning: boolean = false;
backtestResult: BacktestResult | null = null;
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<BacktestDialogComponent>
) {
// Initialize form with defaults
this.backtestForm = this.fb.group({
strategyType: ['', [Validators.required]],
startDate: [new Date(new Date().setFullYear(new Date().getFullYear() - 1)), [Validators.required]],
endDate: [new Date(), [Validators.required]],
initialCapital: [100000, [Validators.required, Validators.min(1000)]],
dataResolution: ['1d', [Validators.required]],
commission: [0.001, [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]]
});
// If strategy is provided, pre-populate the form
if (data) {
this.selectedSymbols = [...data.symbols];
this.backtestForm.patchValue({
strategyType: data.type
});
this.parameters = {...data.parameters};
}
}
ngOnInit(): void {
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
this.strategyService.getStrategyTypes().subscribe({
next: (response) => {
if (response.success) {
this.strategyTypes = response.data;
// If strategy is provided, load its parameters
if (this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: (error) => {
console.error('Error loading strategy types:', error);
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
}
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: (response) => {
if (response.success) {
// If strategy is provided, merge default with existing
if (this.data) {
this.parameters = {
...response.data,
...this.data.parameters
};
} else {
this.parameters = response.data;
}
}
},
error: (error) => {
console.error('Error loading parameters:', error);
this.parameters = {};
}
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) return;
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
onSubmit(): void {
if (this.backtestForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.backtestForm.value;
const backtestRequest: BacktestRequest = {
strategyType: formValue.strategyType,
strategyParams: this.parameters,
symbols: this.selectedSymbols,
startDate: formValue.startDate,
endDate: formValue.endDate,
initialCapital: formValue.initialCapital,
dataResolution: formValue.dataResolution,
commission: formValue.commission,
slippage: formValue.slippage,
mode: formValue.mode
};
this.isRunning = true;
this.strategyService.runBacktest(backtestRequest).subscribe({
next: (response) => {
this.isRunning = false;
if (response.success) {
this.backtestResult = response.data;
}
},
error: (error) => {
this.isRunning = false;
console.error('Backtest error:', error);
}
});
}
close(): void {
this.dialogRef.close(this.backtestResult);
}
}
import { CommonModule } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
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 { 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 { MatTabsModule } from '@angular/material/tabs';
import {
BacktestRequest,
BacktestResult,
StrategyService,
TradingStrategy,
} from '../../../services/strategy.service';
@Component({
selector: 'app-backtest-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
MatNativeDateModule,
MatProgressBarModule,
MatTabsModule,
MatChipsModule,
MatIconModule,
MatSlideToggleModule,
],
templateUrl: './backtest-dialog.component.html',
styleUrl: './backtest-dialog.component.css',
})
export class BacktestDialogComponent implements OnInit {
backtestForm: FormGroup;
strategyTypes: string[] = [];
availableSymbols: string[] = [
'AAPL',
'MSFT',
'GOOGL',
'AMZN',
'TSLA',
'META',
'NVDA',
'SPY',
'QQQ',
];
selectedSymbols: string[] = [];
parameters: Record<string, any> = {};
isRunning: boolean = false;
backtestResult: BacktestResult | null = null;
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<BacktestDialogComponent>
) {
// Initialize form with defaults
this.backtestForm = this.fb.group({
strategyType: ['', [Validators.required]],
startDate: [
new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
[Validators.required],
],
endDate: [new Date(), [Validators.required]],
initialCapital: [100000, [Validators.required, Validators.min(1000)]],
dataResolution: ['1d', [Validators.required]],
commission: [0.001, [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]],
});
// If strategy is provided, pre-populate the form
if (data) {
this.selectedSymbols = [...data.symbols];
this.backtestForm.patchValue({
strategyType: data.type,
});
this.parameters = { ...data.parameters };
}
}
ngOnInit(): void {
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
this.strategyService.getStrategyTypes().subscribe({
next: response => {
if (response.success) {
this.strategyTypes = response.data;
// If strategy is provided, load its parameters
if (this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: error => {
console.error('Error loading strategy types:', error);
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
},
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: response => {
if (response.success) {
// If strategy is provided, merge default with existing
if (this.data) {
this.parameters = {
...response.data,
...this.data.parameters,
};
} else {
this.parameters = response.data;
}
}
},
error: error => {
console.error('Error loading parameters:', error);
this.parameters = {};
},
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) return;
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
onSubmit(): void {
if (this.backtestForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.backtestForm.value;
const backtestRequest: BacktestRequest = {
strategyType: formValue.strategyType,
strategyParams: this.parameters,
symbols: this.selectedSymbols,
startDate: formValue.startDate,
endDate: formValue.endDate,
initialCapital: formValue.initialCapital,
dataResolution: formValue.dataResolution,
commission: formValue.commission,
slippage: formValue.slippage,
mode: formValue.mode,
};
this.isRunning = true;
this.strategyService.runBacktest(backtestRequest).subscribe({
next: response => {
this.isRunning = false;
if (response.success) {
this.backtestResult = response.data;
}
},
error: error => {
this.isRunning = false;
console.error('Backtest error:', error);
},
});
}
close(): void {
this.dialogRef.close(this.backtestResult);
}
}

View file

@ -1,178 +1,180 @@
import { Component, Inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
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 { MatChipsModule } from '@angular/material/chips';
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({
selector: 'app-strategy-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatChipsModule,
MatIconModule,
MatAutocompleteModule
],
templateUrl: './strategy-dialog.component.html',
styleUrl: './strategy-dialog.component.css'
})
export class StrategyDialogComponent implements OnInit {
strategyForm: FormGroup;
isEditMode: boolean = false;
strategyTypes: string[] = [];
availableSymbols: string[] = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'SPY', 'QQQ'];
selectedSymbols: string[] = [];
separatorKeysCodes: number[] = [ENTER, COMMA];
parameters: Record<string, any> = {};
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<StrategyDialogComponent>
) {
this.isEditMode = !!data;
this.strategyForm = this.fb.group({
name: ['', [Validators.required]],
description: [''],
type: ['', [Validators.required]],
// Dynamic parameters will be added based on strategy type
});
if (this.isEditMode && data) {
this.selectedSymbols = [...data.symbols];
this.strategyForm.patchValue({
name: data.name,
description: data.description,
type: data.type
});
this.parameters = {...data.parameters};
}
}
ngOnInit(): void {
// In a real implementation, fetch available strategy types from the API
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
// In a real implementation, this would call the API
this.strategyService.getStrategyTypes().subscribe({
next: (response) => {
if (response.success) {
this.strategyTypes = response.data;
// If editing, load parameters
if (this.isEditMode && this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: (error) => {
console.error('Error loading strategy types:', error);
// Fallback to hardcoded types
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
}
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: (response) => {
if (response.success) {
// If editing, merge default with existing
if (this.isEditMode && this.data) {
this.parameters = {
...response.data,
...this.data.parameters
};
} else {
this.parameters = response.data;
}
}
},
error: (error) => {
console.error('Error loading parameters:', error);
// Fallback to empty parameters
this.parameters = {};
}
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) return;
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
onSubmit(): void {
if (this.strategyForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.strategyForm.value;
const strategy: Partial<TradingStrategy> = {
name: formValue.name,
description: formValue.description,
type: formValue.type,
symbols: this.selectedSymbols,
parameters: this.parameters,
};
if (this.isEditMode && this.data) {
this.strategyService.updateStrategy(this.data.id, strategy).subscribe({
next: (response) => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: (error) => {
console.error('Error updating strategy:', error);
}
});
} else {
this.strategyService.createStrategy(strategy).subscribe({
next: (response) => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: (error) => {
console.error('Error creating strategy:', error);
}
});
}
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
}
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CommonModule } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { StrategyService, TradingStrategy } from '../../../services/strategy.service';
@Component({
selector: 'app-strategy-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatChipsModule,
MatIconModule,
MatAutocompleteModule,
],
templateUrl: './strategy-dialog.component.html',
styleUrl: './strategy-dialog.component.css',
})
export class StrategyDialogComponent implements OnInit {
strategyForm: FormGroup;
isEditMode: boolean = false;
strategyTypes: string[] = [];
availableSymbols: string[] = [
'AAPL',
'MSFT',
'GOOGL',
'AMZN',
'TSLA',
'META',
'NVDA',
'SPY',
'QQQ',
];
selectedSymbols: string[] = [];
separatorKeysCodes: number[] = [ENTER, COMMA];
parameters: Record<string, any> = {};
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<StrategyDialogComponent>
) {
this.isEditMode = !!data;
this.strategyForm = this.fb.group({
name: ['', [Validators.required]],
description: [''],
type: ['', [Validators.required]],
// Dynamic parameters will be added based on strategy type
});
if (this.isEditMode && data) {
this.selectedSymbols = [...data.symbols];
this.strategyForm.patchValue({
name: data.name,
description: data.description,
type: data.type,
});
this.parameters = { ...data.parameters };
}
}
ngOnInit(): void {
// In a real implementation, fetch available strategy types from the API
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
// In a real implementation, this would call the API
this.strategyService.getStrategyTypes().subscribe({
next: response => {
if (response.success) {
this.strategyTypes = response.data;
// If editing, load parameters
if (this.isEditMode && this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: error => {
console.error('Error loading strategy types:', error);
// Fallback to hardcoded types
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
},
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: response => {
if (response.success) {
// If editing, merge default with existing
if (this.isEditMode && this.data) {
this.parameters = {
...response.data,
...this.data.parameters,
};
} else {
this.parameters = response.data;
}
}
},
error: error => {
console.error('Error loading parameters:', error);
// Fallback to empty parameters
this.parameters = {};
},
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) return;
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
onSubmit(): void {
if (this.strategyForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.strategyForm.value;
const strategy: Partial<TradingStrategy> = {
name: formValue.name,
description: formValue.description,
type: formValue.type,
symbols: this.selectedSymbols,
parameters: this.parameters,
};
if (this.isEditMode && this.data) {
this.strategyService.updateStrategy(this.data.id, strategy).subscribe({
next: response => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: error => {
console.error('Error updating strategy:', error);
},
});
} else {
this.strategyService.createStrategy(strategy).subscribe({
next: response => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: error => {
console.error('Error creating strategy:', error);
},
});
}
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
}

View file

@ -1,148 +1,154 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
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 { StrategyService, TradingStrategy } from '../../services/strategy.service';
import { WebSocketService } from '../../services/websocket.service';
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
import { BacktestDialogComponent } from './dialogs/backtest-dialog.component';
import { StrategyDetailsComponent } from './strategy-details/strategy-details.component';
@Component({
selector: 'app-strategies',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTabsModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatDialogModule,
MatMenuModule,
MatChipsModule,
MatProgressBarModule,
FormsModule,
ReactiveFormsModule,
StrategyDetailsComponent
],
templateUrl: './strategies.component.html',
styleUrl: './strategies.component.css'
})
export class StrategiesComponent implements OnInit {
strategies: TradingStrategy[] = [];
displayedColumns: string[] = ['name', 'type', 'symbols', 'status', 'performance', 'actions'];
selectedStrategy: TradingStrategy | null = null;
isLoading = false;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.loadStrategies();
this.listenForStrategyUpdates();
}
loadStrategies(): void {
this.isLoading = true;
this.strategyService.getStrategies().subscribe({
next: (response) => {
if (response.success) {
this.strategies = response.data;
}
this.isLoading = false;
},
error: (error) => {
console.error('Error loading strategies:', error);
this.isLoading = false;
}
});
}
listenForStrategyUpdates(): void {
this.webSocketService.messages.subscribe(message => {
if (message.type === 'STRATEGY_CREATED' ||
message.type === 'STRATEGY_UPDATED' ||
message.type === 'STRATEGY_STATUS_CHANGED') {
// Refresh the strategy list when changes occur
this.loadStrategies();
}
});
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE': return 'green';
case 'PAUSED': return 'orange';
case 'ERROR': return 'red';
default: return 'gray';
}
}
openStrategyDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: strategy || null
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadStrategies();
}
});
}
openBacktestDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: strategy || null
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Handle backtest result if needed
}
});
}
toggleStrategyStatus(strategy: TradingStrategy): void {
this.isLoading = true;
if (strategy.status === 'ACTIVE') {
this.strategyService.pauseStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: (error) => {
console.error('Error pausing strategy:', error);
this.isLoading = false;
}
});
} else {
this.strategyService.startStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: (error) => {
console.error('Error starting strategy:', error);
this.isLoading = false;
}
});
}
}
viewStrategyDetails(strategy: TradingStrategy): void {
this.selectedStrategy = strategy;
}
}
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
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 { WebSocketService } from '../../services/websocket.service';
import { BacktestDialogComponent } from './dialogs/backtest-dialog.component';
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
import { StrategyDetailsComponent } from './strategy-details/strategy-details.component';
@Component({
selector: 'app-strategies',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTabsModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatDialogModule,
MatMenuModule,
MatChipsModule,
MatProgressBarModule,
FormsModule,
ReactiveFormsModule,
StrategyDetailsComponent,
],
templateUrl: './strategies.component.html',
styleUrl: './strategies.component.css',
})
export class StrategiesComponent implements OnInit {
strategies: TradingStrategy[] = [];
displayedColumns: string[] = ['name', 'type', 'symbols', 'status', 'performance', 'actions'];
selectedStrategy: TradingStrategy | null = null;
isLoading = false;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.loadStrategies();
this.listenForStrategyUpdates();
}
loadStrategies(): void {
this.isLoading = true;
this.strategyService.getStrategies().subscribe({
next: response => {
if (response.success) {
this.strategies = response.data;
}
this.isLoading = false;
},
error: error => {
console.error('Error loading strategies:', error);
this.isLoading = false;
},
});
}
listenForStrategyUpdates(): void {
this.webSocketService.messages.subscribe(message => {
if (
message.type === 'STRATEGY_CREATED' ||
message.type === 'STRATEGY_UPDATED' ||
message.type === 'STRATEGY_STATUS_CHANGED'
) {
// Refresh the strategy list when changes occur
this.loadStrategies();
}
});
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE':
return 'green';
case 'PAUSED':
return 'orange';
case 'ERROR':
return 'red';
default:
return 'gray';
}
}
openStrategyDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: strategy || null,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadStrategies();
}
});
}
openBacktestDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: strategy || null,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Handle backtest result if needed
}
});
}
toggleStrategyStatus(strategy: TradingStrategy): void {
this.isLoading = true;
if (strategy.status === 'ACTIVE') {
this.strategyService.pauseStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: error => {
console.error('Error pausing strategy:', error);
this.isLoading = false;
},
});
} else {
this.strategyService.startStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: error => {
console.error('Error starting strategy:', error);
this.isLoading = false;
},
});
}
}
viewStrategyDetails(strategy: TradingStrategy): void {
this.selectedStrategy = strategy;
}
}

View file

@ -4,122 +4,144 @@
<mat-card class="flex-1 p-4">
<div class="flex items-start justify-between">
<div>
<h2 class="text-xl font-bold">{{strategy.name}}</h2>
<p class="text-gray-600 text-sm">{{strategy.description}}</p>
<h2 class="text-xl font-bold">{{ strategy.name }}</h2>
<p class="text-gray-600 text-sm">{{ strategy.description }}</p>
</div>
<div class="flex items-center gap-2">
<button mat-raised-button color="primary" class="mr-2" (click)="openBacktestDialog()">
Run Backtest
</button>
<span class="px-3 py-1 rounded-full text-xs font-semibold"
[style.background-color]="getStatusColor(strategy.status)"
style="color: white;">
{{strategy.status}}
<span
class="px-3 py-1 rounded-full text-xs font-semibold"
[style.background-color]="getStatusColor(strategy.status)"
style="color: white"
>
{{ strategy.status }}
</span>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold text-sm text-gray-600">Type</h3>
<p>{{strategy.type}}</p>
<p>{{ strategy.type }}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Created</h3>
<p>{{strategy.createdAt | date:'medium'}}</p>
<p>{{ strategy.createdAt | date: 'medium' }}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Last Updated</h3>
<p>{{strategy.updatedAt | date:'medium'}}</p>
<p>{{ strategy.updatedAt | date: 'medium' }}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Symbols</h3>
<div class="flex flex-wrap gap-1 mt-1">
<mat-chip *ngFor="let symbol of strategy.symbols">{{symbol}}</mat-chip>
<mat-chip *ngFor="let symbol of strategy.symbols">{{ symbol }}</mat-chip>
</div>
</div>
</div>
</mat-card>
<!-- Performance Summary Card -->
<mat-card class="md:w-1/3 p-4">
<h3 class="text-lg font-bold mb-3">Performance</h3>
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-sm text-gray-600">Return</p>
<p class="text-xl font-semibold"
[ngClass]="{'text-green-600': performance.totalReturn >= 0, 'text-red-600': performance.totalReturn < 0}">
{{performance.totalReturn | percent:'1.2-2'}}
<p
class="text-xl font-semibold"
[ngClass]="{
'text-green-600': performance.totalReturn >= 0,
'text-red-600': performance.totalReturn < 0,
}"
>
{{ performance.totalReturn | percent: '1.2-2' }}
</p>
</div>
<div>
<p class="text-sm text-gray-600">Win Rate</p>
<p class="text-xl font-semibold">{{performance.winRate | percent:'1.0-0'}}</p>
<p class="text-xl font-semibold">{{ performance.winRate | percent: '1.0-0' }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Sharpe Ratio</p>
<p class="text-xl font-semibold">{{performance.sharpeRatio | number:'1.2-2'}}</p>
<p class="text-xl font-semibold">{{ performance.sharpeRatio | number: '1.2-2' }}</p>
</div>
<div>
<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>
<p class="text-sm text-gray-600">Total Trades</p>
<p class="text-xl font-semibold">{{performance.totalTrades}}</p>
<p class="text-xl font-semibold">{{ performance.totalTrades }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Sortino Ratio</p>
<p class="text-xl font-semibold">{{performance.sortinoRatio | number:'1.2-2'}}</p>
<p class="text-xl font-semibold">{{ performance.sortinoRatio | number: '1.2-2' }}</p>
</div>
</div>
<mat-divider class="my-4"></mat-divider>
<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
</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
</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
</button>
<button mat-button (click)="openEditDialog()">
<mat-icon>edit</mat-icon> Edit
</button>
<button mat-button (click)="openEditDialog()"><mat-icon>edit</mat-icon> Edit</button>
</div>
</mat-card>
</div>
<!-- Parameters Card -->
<mat-card class="p-4">
<h3 class="text-lg font-bold mb-3">Strategy Parameters</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div *ngFor="let param of strategy.parameters | keyvalue">
<p class="text-sm text-gray-600">{{param.key}}</p>
<p class="font-semibold">{{param.value}}</p>
<p class="text-sm text-gray-600">{{ param.key }}</p>
<p class="font-semibold">{{ param.value }}</p>
</div>
</div>
</mat-card>
<!-- Backtest Results Section (only shown when a backtest has been run) -->
<!-- Backtest Results Section (only shown when a backtest has been run) -->
<div *ngIf="backtestResult" class="backtest-results space-y-6">
<h2 class="text-xl font-bold">Backtest Results</h2>
<!-- Performance Metrics Component -->
<app-performance-metrics [backtestResult]="backtestResult"></app-performance-metrics>
<!-- Equity Chart Component -->
<app-equity-chart [backtestResult]="backtestResult"></app-equity-chart>
<!-- Drawdown Chart Component -->
<app-drawdown-chart [backtestResult]="backtestResult"></app-drawdown-chart>
<!-- Trades Table Component -->
<app-trades-table [backtestResult]="backtestResult"></app-trades-table>
</div>
<!-- Tabs for Signals/Trades -->
<mat-card class="p-0">
<mat-tab-group>
@ -140,18 +162,20 @@
</thead>
<tbody>
<tr *ngFor="let signal of signals">
<td class="py-2">{{signal.timestamp | date:'short'}}</td>
<td class="py-2">{{signal.symbol}}</td>
<td class="py-2">{{ signal.timestamp | date: 'short' }}</td>
<td class="py-2">{{ signal.symbol }}</td>
<td class="py-2">
<span class="px-2 py-1 rounded text-xs font-semibold"
[style.background-color]="getSignalColor(signal.action)"
style="color: white;">
{{signal.action}}
<span
class="px-2 py-1 rounded text-xs font-semibold"
[style.background-color]="getSignalColor(signal.action)"
style="color: white"
>
{{ signal.action }}
</span>
</td>
<td class="py-2">${{signal.price | number:'1.2-2'}}</td>
<td class="py-2">{{signal.quantity}}</td>
<td class="py-2">{{signal.confidence | percent:'1.0-0'}}</td>
<td class="py-2">${{ signal.price | number: '1.2-2' }}</td>
<td class="py-2">{{ signal.quantity }}</td>
<td class="py-2">{{ signal.confidence | percent: '1.0-0' }}</td>
</tr>
</tbody>
</table>
@ -161,7 +185,7 @@
</ng-template>
</div>
</mat-tab>
<!-- Trades Tab -->
<mat-tab label="Recent Trades">
<div class="p-4">
@ -179,19 +203,30 @@
</thead>
<tbody>
<tr *ngFor="let trade of trades">
<td class="py-2">{{trade.symbol}}</td>
<td class="py-2">{{ trade.symbol }}</td>
<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 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 class="py-2">{{trade.quantity}}</td>
<td class="py-2" [ngClass]="{'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0}">
${{trade.pnl | number:'1.2-2'}}
<td class="py-2">{{ trade.quantity }}</td>
<td
class="py-2"
[ngClass]="{ 'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0 }"
>
${{ trade.pnl | number: '1.2-2' }}
</td>
<td class="py-2" [ngClass]="{'text-green-600': trade.pnlPercent >= 0, 'text-red-600': trade.pnlPercent < 0}">
{{trade.pnlPercent | number:'1.2-2'}}%
<td
class="py-2"
[ngClass]="{
'text-green-600': trade.pnlPercent >= 0,
'text-red-600': trade.pnlPercent < 0,
}"
>
{{ trade.pnlPercent | number: '1.2-2' }}%
</td>
</tr>
</tbody>
@ -208,7 +243,7 @@
<mat-card class="p-6 flex items-center" *ngIf="!strategy">
<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>
</div>
</mat-card>

View file

@ -1,381 +1,389 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
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 { BacktestResult, TradingStrategy, StrategyService } from '../../../services/strategy.service';
import { WebSocketService } from '../../../services/websocket.service';
import { EquityChartComponent } from '../components/equity-chart.component';
import { DrawdownChartComponent } from '../components/drawdown-chart.component';
import { TradesTableComponent } from '../components/trades-table.component';
import { PerformanceMetricsComponent } from '../components/performance-metrics.component';
import { StrategyDialogComponent } from '../dialogs/strategy-dialog.component';
import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
@Component({
selector: 'app-strategy-details',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatChipsModule,
MatProgressBarModule,
MatDividerModule,
EquityChartComponent,
DrawdownChartComponent,
TradesTableComponent,
PerformanceMetricsComponent
],
templateUrl: './strategy-details.component.html',
styleUrl: './strategy-details.component.css'
})
export class StrategyDetailsComponent implements OnChanges {
@Input() strategy: TradingStrategy | null = null;
signals: any[] = [];
trades: any[] = [];
performance: any = {};
isLoadingSignals = false;
isLoadingTrades = false;
backtestResult: BacktestResult | undefined;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['strategy'] && this.strategy) {
this.loadStrategyData();
this.listenForUpdates();
}
}
loadStrategyData(): void {
if (!this.strategy) return;
// In a real implementation, these would call API methods to fetch the data
this.loadSignals();
this.loadTrades();
this.loadPerformance();
}
loadSignals(): void {
if (!this.strategy) return;
this.isLoadingSignals = true;
// First check if we can get real signals from the API
this.strategyService.getStrategySignals(this.strategy.id)
.subscribe({
next: (response) => {
if (response.success && response.data && response.data.length > 0) {
this.signals = response.data;
} else {
// Fallback to mock data if no real signals available
this.signals = this.generateMockSignals();
}
this.isLoadingSignals = false;
},
error: (error) => {
console.error('Error loading signals', error);
// Fallback to mock data on error
this.signals = this.generateMockSignals();
this.isLoadingSignals = false;
}
});
}
loadTrades(): void {
if (!this.strategy) return;
this.isLoadingTrades = true;
// First check if we can get real trades from the API
this.strategyService.getStrategyTrades(this.strategy.id)
.subscribe({
next: (response) => {
if (response.success && response.data && response.data.length > 0) {
this.trades = response.data;
} else {
// Fallback to mock data if no real trades available
this.trades = this.generateMockTrades();
}
this.isLoadingTrades = false;
},
error: (error) => {
console.error('Error loading trades', error);
// Fallback to mock data on error
this.trades = this.generateMockTrades();
this.isLoadingTrades = false;
}
});
}
loadPerformance(): void {
// This would be an API call in a real implementation
this.performance = {
totalReturn: this.strategy?.performance.totalReturn || 0,
winRate: this.strategy?.performance.winRate || 0,
sharpeRatio: this.strategy?.performance.sharpeRatio || 0,
maxDrawdown: this.strategy?.performance.maxDrawdown || 0,
totalTrades: this.strategy?.performance.totalTrades || 0,
// Additional metrics that would come from the API
dailyReturn: 0.0012,
volatility: 0.008,
sortinoRatio: 1.2,
calmarRatio: 0.7
};
}
listenForUpdates(): void {
if (!this.strategy) return;
// Subscribe to strategy signals
this.webSocketService.getStrategySignals(this.strategy.id)
.subscribe((signal: any) => {
// Add the new signal to the top of the list
this.signals = [signal, ...this.signals.slice(0, 9)]; // Keep only the latest 10 signals
});
// Subscribe to strategy trades
this.webSocketService.getStrategyTrades(this.strategy.id)
.subscribe((trade: any) => {
// Add the new trade to the top of the list
this.trades = [trade, ...this.trades.slice(0, 9)]; // Keep only the latest 10 trades
// Update performance metrics
this.updatePerformanceMetrics();
});
// Subscribe to strategy status updates
this.webSocketService.getStrategyUpdates()
.subscribe((update: any) => {
if (update.strategyId === this.strategy?.id) {
// Update strategy status if changed
if (update.status && this.strategy && this.strategy.status !== update.status) {
this.strategy.status = update.status;
}
// Update other fields if present
if (update.performance && this.strategy) {
this.strategy.performance = {
...this.strategy.performance,
...update.performance
};
this.performance = {
...this.performance,
...update.performance
};
}
}
});
console.log('WebSocket listeners for strategy updates initialized');
}
/**
* Update performance metrics when new trades come in
*/
private updatePerformanceMetrics(): void {
if (!this.strategy || this.trades.length === 0) return;
// Calculate basic metrics
const winningTrades = this.trades.filter(t => t.pnl > 0);
const losingTrades = this.trades.filter(t => t.pnl < 0);
const totalPnl = this.trades.reduce((sum, trade) => sum + trade.pnl, 0);
const winRate = winningTrades.length / this.trades.length;
// Update performance data
const currentPerformance = this.performance || {};
this.performance = {
...currentPerformance,
totalTrades: this.trades.length,
winRate: winRate,
totalReturn: (currentPerformance.totalReturn || 0) + (totalPnl / 10000) // Approximate
};
// Update strategy performance as well
if (this.strategy && this.strategy.performance) {
this.strategy.performance = {
...this.strategy.performance,
totalTrades: this.trades.length,
winRate: winRate
};
}
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE': return 'green';
case 'PAUSED': return 'orange';
case 'ERROR': return 'red';
default: return 'gray';
}
}
getSignalColor(action: string): string {
switch (action) {
case 'BUY': return 'green';
case 'SELL': return 'red';
default: return 'gray';
}
}
/**
* Open the backtest dialog to run a backtest for this strategy
*/
openBacktestDialog(): void {
if (!this.strategy) return;
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: this.strategy
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Store the backtest result for visualization
this.backtestResult = result;
}
});
}
/**
* Open the strategy edit dialog
*/
openEditDialog(): void {
if (!this.strategy) return;
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: this.strategy
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Refresh strategy data after edit
this.loadStrategyData();
}
});
}
/**
* Start the strategy
*/
activateStrategy(): void {
if (!this.strategy) return;
this.strategyService.startStrategy(this.strategy.id).subscribe({
next: (response) => {
if (response.success) {
this.strategy!.status = 'ACTIVE';
}
},
error: (error) => {
console.error('Error starting strategy:', error);
}
});
}
/**
* Pause the strategy
*/
pauseStrategy(): void {
if (!this.strategy) return;
this.strategyService.pauseStrategy(this.strategy.id).subscribe({
next: (response) => {
if (response.success) {
this.strategy!.status = 'PAUSED';
}
},
error: (error) => {
console.error('Error pausing strategy:', error);
}
});
}
/**
* Stop the strategy
*/
stopStrategy(): void {
if (!this.strategy) return;
this.strategyService.stopStrategy(this.strategy.id).subscribe({
next: (response) => {
if (response.success) {
this.strategy!.status = 'INACTIVE';
}
},
error: (error) => {
console.error('Error stopping strategy:', error);
}
});
}
// Methods to generate mock data
private generateMockSignals(): any[] {
if (!this.strategy) return [];
const signals = [];
const actions = ['BUY', 'SELL', 'HOLD'];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol = this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const action = actions[Math.floor(Math.random() * actions.length)];
signals.push({
id: `sig_${i}`,
symbol,
action,
confidence: 0.7 + Math.random() * 0.3,
price: 100 + Math.random() * 50,
timestamp: new Date(now.getTime() - i * 1000 * 60 * 30), // 30 min intervals
quantity: Math.floor(10 + Math.random() * 90)
});
}
return signals;
}
private generateMockTrades(): any[] {
if (!this.strategy) return [];
const trades = [];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol = this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const entryPrice = 100 + Math.random() * 50;
const exitPrice = entryPrice * (1 + (Math.random() * 0.1 - 0.05)); // -5% to +5%
const quantity = Math.floor(10 + Math.random() * 90);
const pnl = (exitPrice - entryPrice) * quantity;
trades.push({
id: `trade_${i}`,
symbol,
entryPrice,
entryTime: new Date(now.getTime() - (i + 5) * 1000 * 60 * 60), // Hourly intervals
exitPrice,
exitTime: new Date(now.getTime() - i * 1000 * 60 * 60),
quantity,
pnl,
pnlPercent: ((exitPrice - entryPrice) / entryPrice) * 100
});
}
return trades;
}
}
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
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 { DrawdownChartComponent } from '../components/drawdown-chart.component';
import { EquityChartComponent } from '../components/equity-chart.component';
import { PerformanceMetricsComponent } from '../components/performance-metrics.component';
import { TradesTableComponent } from '../components/trades-table.component';
import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
import { StrategyDialogComponent } from '../dialogs/strategy-dialog.component';
@Component({
selector: 'app-strategy-details',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatChipsModule,
MatProgressBarModule,
MatDividerModule,
EquityChartComponent,
DrawdownChartComponent,
TradesTableComponent,
PerformanceMetricsComponent,
],
templateUrl: './strategy-details.component.html',
styleUrl: './strategy-details.component.css',
})
export class StrategyDetailsComponent implements OnChanges {
@Input() strategy: TradingStrategy | null = null;
signals: any[] = [];
trades: any[] = [];
performance: any = {};
isLoadingSignals = false;
isLoadingTrades = false;
backtestResult: BacktestResult | undefined;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['strategy'] && this.strategy) {
this.loadStrategyData();
this.listenForUpdates();
}
}
loadStrategyData(): void {
if (!this.strategy) return;
// In a real implementation, these would call API methods to fetch the data
this.loadSignals();
this.loadTrades();
this.loadPerformance();
}
loadSignals(): void {
if (!this.strategy) return;
this.isLoadingSignals = true;
// First check if we can get real signals from the API
this.strategyService.getStrategySignals(this.strategy.id).subscribe({
next: response => {
if (response.success && response.data && response.data.length > 0) {
this.signals = response.data;
} else {
// Fallback to mock data if no real signals available
this.signals = this.generateMockSignals();
}
this.isLoadingSignals = false;
},
error: error => {
console.error('Error loading signals', error);
// Fallback to mock data on error
this.signals = this.generateMockSignals();
this.isLoadingSignals = false;
},
});
}
loadTrades(): void {
if (!this.strategy) return;
this.isLoadingTrades = true;
// First check if we can get real trades from the API
this.strategyService.getStrategyTrades(this.strategy.id).subscribe({
next: response => {
if (response.success && response.data && response.data.length > 0) {
this.trades = response.data;
} else {
// Fallback to mock data if no real trades available
this.trades = this.generateMockTrades();
}
this.isLoadingTrades = false;
},
error: error => {
console.error('Error loading trades', error);
// Fallback to mock data on error
this.trades = this.generateMockTrades();
this.isLoadingTrades = false;
},
});
}
loadPerformance(): void {
// This would be an API call in a real implementation
this.performance = {
totalReturn: this.strategy?.performance.totalReturn || 0,
winRate: this.strategy?.performance.winRate || 0,
sharpeRatio: this.strategy?.performance.sharpeRatio || 0,
maxDrawdown: this.strategy?.performance.maxDrawdown || 0,
totalTrades: this.strategy?.performance.totalTrades || 0,
// Additional metrics that would come from the API
dailyReturn: 0.0012,
volatility: 0.008,
sortinoRatio: 1.2,
calmarRatio: 0.7,
};
}
listenForUpdates(): void {
if (!this.strategy) return;
// Subscribe to strategy signals
this.webSocketService.getStrategySignals(this.strategy.id).subscribe((signal: any) => {
// Add the new signal to the top of the list
this.signals = [signal, ...this.signals.slice(0, 9)]; // Keep only the latest 10 signals
});
// Subscribe to strategy trades
this.webSocketService.getStrategyTrades(this.strategy.id).subscribe((trade: any) => {
// Add the new trade to the top of the list
this.trades = [trade, ...this.trades.slice(0, 9)]; // Keep only the latest 10 trades
// Update performance metrics
this.updatePerformanceMetrics();
});
// Subscribe to strategy status updates
this.webSocketService.getStrategyUpdates().subscribe((update: any) => {
if (update.strategyId === this.strategy?.id) {
// Update strategy status if changed
if (update.status && this.strategy && this.strategy.status !== update.status) {
this.strategy.status = update.status;
}
// Update other fields if present
if (update.performance && this.strategy) {
this.strategy.performance = {
...this.strategy.performance,
...update.performance,
};
this.performance = {
...this.performance,
...update.performance,
};
}
}
});
console.log('WebSocket listeners for strategy updates initialized');
}
/**
* Update performance metrics when new trades come in
*/
private updatePerformanceMetrics(): void {
if (!this.strategy || this.trades.length === 0) return;
// Calculate basic metrics
const winningTrades = this.trades.filter(t => t.pnl > 0);
const losingTrades = this.trades.filter(t => t.pnl < 0);
const totalPnl = this.trades.reduce((sum, trade) => sum + trade.pnl, 0);
const winRate = winningTrades.length / this.trades.length;
// Update performance data
const currentPerformance = this.performance || {};
this.performance = {
...currentPerformance,
totalTrades: this.trades.length,
winRate: winRate,
totalReturn: (currentPerformance.totalReturn || 0) + totalPnl / 10000, // Approximate
};
// Update strategy performance as well
if (this.strategy && this.strategy.performance) {
this.strategy.performance = {
...this.strategy.performance,
totalTrades: this.trades.length,
winRate: winRate,
};
}
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE':
return 'green';
case 'PAUSED':
return 'orange';
case 'ERROR':
return 'red';
default:
return 'gray';
}
}
getSignalColor(action: string): string {
switch (action) {
case 'BUY':
return 'green';
case 'SELL':
return 'red';
default:
return 'gray';
}
}
/**
* Open the backtest dialog to run a backtest for this strategy
*/
openBacktestDialog(): void {
if (!this.strategy) return;
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: this.strategy,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Store the backtest result for visualization
this.backtestResult = result;
}
});
}
/**
* Open the strategy edit dialog
*/
openEditDialog(): void {
if (!this.strategy) return;
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: this.strategy,
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Refresh strategy data after edit
this.loadStrategyData();
}
});
}
/**
* Start the strategy
*/
activateStrategy(): void {
if (!this.strategy) return;
this.strategyService.startStrategy(this.strategy.id).subscribe({
next: response => {
if (response.success) {
this.strategy!.status = 'ACTIVE';
}
},
error: error => {
console.error('Error starting strategy:', error);
},
});
}
/**
* Pause the strategy
*/
pauseStrategy(): void {
if (!this.strategy) return;
this.strategyService.pauseStrategy(this.strategy.id).subscribe({
next: response => {
if (response.success) {
this.strategy!.status = 'PAUSED';
}
},
error: error => {
console.error('Error pausing strategy:', error);
},
});
}
/**
* Stop the strategy
*/
stopStrategy(): void {
if (!this.strategy) return;
this.strategyService.stopStrategy(this.strategy.id).subscribe({
next: response => {
if (response.success) {
this.strategy!.status = 'INACTIVE';
}
},
error: error => {
console.error('Error stopping strategy:', error);
},
});
}
// Methods to generate mock data
private generateMockSignals(): any[] {
if (!this.strategy) return [];
const signals = [];
const actions = ['BUY', 'SELL', 'HOLD'];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol =
this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const action = actions[Math.floor(Math.random() * actions.length)];
signals.push({
id: `sig_${i}`,
symbol,
action,
confidence: 0.7 + Math.random() * 0.3,
price: 100 + Math.random() * 50,
timestamp: new Date(now.getTime() - i * 1000 * 60 * 30), // 30 min intervals
quantity: Math.floor(10 + Math.random() * 90),
});
}
return signals;
}
private generateMockTrades(): any[] {
if (!this.strategy) return [];
const trades = [];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol =
this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const entryPrice = 100 + Math.random() * 50;
const exitPrice = entryPrice * (1 + (Math.random() * 0.1 - 0.05)); // -5% to +5%
const quantity = Math.floor(10 + Math.random() * 90);
const pnl = (exitPrice - entryPrice) * quantity;
trades.push({
id: `trade_${i}`,
symbol,
entryPrice,
entryTime: new Date(now.getTime() - (i + 5) * 1000 * 60 * 60), // Hourly intervals
exitPrice,
exitTime: new Date(now.getTime() - i * 1000 * 60 * 60),
quantity,
pnl,
pnlPercent: ((exitPrice - entryPrice) / entryPrice) * 100,
});
}
return trades;
}
}

View file

@ -1,98 +1,104 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface RiskThresholds {
maxPositionSize: number;
maxDailyLoss: number;
maxPortfolioRisk: number;
volatilityLimit: number;
}
export interface RiskEvaluation {
symbol: string;
positionValue: number;
positionRisk: number;
violations: string[];
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export interface MarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private readonly baseUrls = {
riskGuardian: 'http://localhost:3002',
strategyOrchestrator: 'http://localhost:3003',
marketDataGateway: 'http://localhost:3001'
};
constructor(private http: HttpClient) {}
// Risk Guardian API
getRiskThresholds(): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.get<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`
);
}
updateRiskThresholds(thresholds: RiskThresholds): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.put<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`,
thresholds
);
}
evaluateRisk(params: {
symbol: string;
quantity: number;
price: number;
portfolioValue: number;
}): Observable<{ success: boolean; data: RiskEvaluation }> {
return this.http.post<{ success: boolean; data: RiskEvaluation }>(
`${this.baseUrls.riskGuardian}/api/risk/evaluate`,
params
);
}
getRiskHistory(): Observable<{ success: boolean; data: RiskEvaluation[] }> {
return this.http.get<{ success: boolean; data: RiskEvaluation[] }>(
`${this.baseUrls.riskGuardian}/api/risk/history`
);
}
// Strategy Orchestrator API
getStrategies(): Observable<{ success: boolean; data: any[] }> {
return this.http.get<{ success: boolean; data: any[] }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`
);
}
createStrategy(strategy: any): Observable<{ success: boolean; data: any }> {
return this.http.post<{ success: boolean; data: any }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`,
strategy
);
}
// Market Data Gateway API
getMarketData(symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']): Observable<{ success: boolean; data: MarketData[] }> {
const symbolsParam = symbols.join(',');
return this.http.get<{ success: boolean; data: MarketData[] }>(
`${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}`
);
}
// Health checks
checkServiceHealth(service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'): Observable<any> {
return this.http.get(`${this.baseUrls[service]}/health`);
}
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface RiskThresholds {
maxPositionSize: number;
maxDailyLoss: number;
maxPortfolioRisk: number;
volatilityLimit: number;
}
export interface RiskEvaluation {
symbol: string;
positionValue: number;
positionRisk: number;
violations: string[];
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export interface MarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly baseUrls = {
riskGuardian: 'http://localhost:3002',
strategyOrchestrator: 'http://localhost:3003',
marketDataGateway: 'http://localhost:3001',
};
constructor(private http: HttpClient) {}
// Risk Guardian API
getRiskThresholds(): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.get<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`
);
}
updateRiskThresholds(
thresholds: RiskThresholds
): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.put<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`,
thresholds
);
}
evaluateRisk(params: {
symbol: string;
quantity: number;
price: number;
portfolioValue: number;
}): Observable<{ success: boolean; data: RiskEvaluation }> {
return this.http.post<{ success: boolean; data: RiskEvaluation }>(
`${this.baseUrls.riskGuardian}/api/risk/evaluate`,
params
);
}
getRiskHistory(): Observable<{ success: boolean; data: RiskEvaluation[] }> {
return this.http.get<{ success: boolean; data: RiskEvaluation[] }>(
`${this.baseUrls.riskGuardian}/api/risk/history`
);
}
// Strategy Orchestrator API
getStrategies(): Observable<{ success: boolean; data: any[] }> {
return this.http.get<{ success: boolean; data: any[] }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`
);
}
createStrategy(strategy: any): Observable<{ success: boolean; data: any }> {
return this.http.post<{ success: boolean; data: any }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`,
strategy
);
}
// Market Data Gateway API
getMarketData(
symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']
): Observable<{ success: boolean; data: MarketData[] }> {
const symbolsParam = symbols.join(',');
return this.http.get<{ success: boolean; data: MarketData[] }>(
`${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}`
);
}
// Health checks
checkServiceHealth(
service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'
): Observable<any> {
return this.http.get(`${this.baseUrls[service]}/health`);
}
}

View file

@ -1,193 +1,191 @@
import { Injectable, signal, inject } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { WebSocketService, RiskAlert } from './websocket.service';
import { Subscription } from 'rxjs';
export interface Notification {
id: string;
type: 'info' | 'warning' | 'error' | 'success';
title: string;
message: string;
timestamp: Date;
read: boolean;
}
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private snackBar = inject(MatSnackBar);
private webSocketService = inject(WebSocketService);
private riskAlertsSubscription?: Subscription;
// Reactive state
public notifications = signal<Notification[]>([]);
public unreadCount = signal<number>(0);
constructor() {
this.initializeRiskAlerts();
}
private initializeRiskAlerts() {
// Subscribe to risk alerts from WebSocket
this.riskAlertsSubscription = this.webSocketService.getRiskAlerts().subscribe({
next: (alert: RiskAlert) => {
this.handleRiskAlert(alert);
},
error: (err) => {
console.error('Risk alert subscription error:', err);
}
});
}
private handleRiskAlert(alert: RiskAlert) {
const notification: Notification = {
id: alert.id,
type: this.mapSeverityToType(alert.severity),
title: `Risk Alert: ${alert.symbol}`,
message: alert.message,
timestamp: new Date(alert.timestamp),
read: false
};
this.addNotification(notification);
this.showSnackBarAlert(notification);
}
private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' {
switch (severity) {
case 'HIGH': return 'error';
case 'MEDIUM': return 'warning';
case 'LOW': return 'info';
default: return 'info';
}
}
private showSnackBarAlert(notification: Notification) {
const actionText = notification.type === 'error' ? 'Review' : 'Dismiss';
const duration = notification.type === 'error' ? 10000 : 5000;
this.snackBar.open(
`${notification.title}: ${notification.message}`,
actionText,
{
duration,
panelClass: [`snack-${notification.type}`]
}
);
}
// Public methods
addNotification(notification: Notification) {
const current = this.notifications();
const updated = [notification, ...current].slice(0, 50); // Keep only latest 50
this.notifications.set(updated);
this.updateUnreadCount();
}
markAsRead(notificationId: string) {
const current = this.notifications();
const updated = current.map(n =>
n.id === notificationId ? { ...n, read: true } : n
);
this.notifications.set(updated);
this.updateUnreadCount();
}
markAllAsRead() {
const current = this.notifications();
const updated = current.map(n => ({ ...n, read: true }));
this.notifications.set(updated);
this.updateUnreadCount();
}
clearNotification(notificationId: string) {
const current = this.notifications();
const updated = current.filter(n => n.id !== notificationId);
this.notifications.set(updated);
this.updateUnreadCount();
}
clearAllNotifications() {
this.notifications.set([]);
this.unreadCount.set(0);
}
private updateUnreadCount() {
const unread = this.notifications().filter(n => !n.read).length;
this.unreadCount.set(unread);
}
// Manual notification methods
showSuccess(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'success',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 3000,
panelClass: ['snack-success']
});
}
showError(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'error',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 8000,
panelClass: ['snack-error']
});
}
showWarning(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'warning',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 5000,
panelClass: ['snack-warning']
});
}
showInfo(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'info',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 4000,
panelClass: ['snack-info']
});
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
ngOnDestroy() {
this.riskAlertsSubscription?.unsubscribe();
}
}
import { inject, Injectable, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subscription } from 'rxjs';
import { RiskAlert, WebSocketService } from './websocket.service';
export interface Notification {
id: string;
type: 'info' | 'warning' | 'error' | 'success';
title: string;
message: string;
timestamp: Date;
read: boolean;
}
@Injectable({
providedIn: 'root',
})
export class NotificationService {
private snackBar = inject(MatSnackBar);
private webSocketService = inject(WebSocketService);
private riskAlertsSubscription?: Subscription;
// Reactive state
public notifications = signal<Notification[]>([]);
public unreadCount = signal<number>(0);
constructor() {
this.initializeRiskAlerts();
}
private initializeRiskAlerts() {
// Subscribe to risk alerts from WebSocket
this.riskAlertsSubscription = this.webSocketService.getRiskAlerts().subscribe({
next: (alert: RiskAlert) => {
this.handleRiskAlert(alert);
},
error: err => {
console.error('Risk alert subscription error:', err);
},
});
}
private handleRiskAlert(alert: RiskAlert) {
const notification: Notification = {
id: alert.id,
type: this.mapSeverityToType(alert.severity),
title: `Risk Alert: ${alert.symbol}`,
message: alert.message,
timestamp: new Date(alert.timestamp),
read: false,
};
this.addNotification(notification);
this.showSnackBarAlert(notification);
}
private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' {
switch (severity) {
case 'HIGH':
return 'error';
case 'MEDIUM':
return 'warning';
case 'LOW':
return 'info';
default:
return 'info';
}
}
private showSnackBarAlert(notification: Notification) {
const actionText = notification.type === 'error' ? 'Review' : 'Dismiss';
const duration = notification.type === 'error' ? 10000 : 5000;
this.snackBar.open(`${notification.title}: ${notification.message}`, actionText, {
duration,
panelClass: [`snack-${notification.type}`],
});
}
// Public methods
addNotification(notification: Notification) {
const current = this.notifications();
const updated = [notification, ...current].slice(0, 50); // Keep only latest 50
this.notifications.set(updated);
this.updateUnreadCount();
}
markAsRead(notificationId: string) {
const current = this.notifications();
const updated = current.map(n => (n.id === notificationId ? { ...n, read: true } : n));
this.notifications.set(updated);
this.updateUnreadCount();
}
markAllAsRead() {
const current = this.notifications();
const updated = current.map(n => ({ ...n, read: true }));
this.notifications.set(updated);
this.updateUnreadCount();
}
clearNotification(notificationId: string) {
const current = this.notifications();
const updated = current.filter(n => n.id !== notificationId);
this.notifications.set(updated);
this.updateUnreadCount();
}
clearAllNotifications() {
this.notifications.set([]);
this.unreadCount.set(0);
}
private updateUnreadCount() {
const unread = this.notifications().filter(n => !n.read).length;
this.unreadCount.set(unread);
}
// Manual notification methods
showSuccess(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'success',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 3000,
panelClass: ['snack-success'],
});
}
showError(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'error',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 8000,
panelClass: ['snack-error'],
});
}
showWarning(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'warning',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 5000,
panelClass: ['snack-warning'],
});
}
showInfo(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'info',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 4000,
panelClass: ['snack-info'],
});
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
ngOnDestroy() {
this.riskAlertsSubscription?.unsubscribe();
}
}

View file

@ -1,209 +1,238 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface TradingStrategy {
id: string;
name: string;
description: string;
status: 'ACTIVE' | 'INACTIVE' | 'PAUSED' | 'ERROR';
type: string;
symbols: string[];
parameters: Record<string, any>;
performance: {
totalTrades: number;
winRate: number;
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
};
createdAt: Date;
updatedAt: Date;
}
export interface BacktestRequest {
strategyType: string;
strategyParams: Record<string, any>;
symbols: string[];
startDate: Date | string;
endDate: Date | string;
initialCapital: number;
dataResolution: '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
commission: number;
slippage: number;
mode: 'event' | 'vector';
}
export interface BacktestResult {
strategyId: string;
startDate: Date;
endDate: Date;
duration: number;
initialCapital: number;
finalCapital: number;
totalReturn: number;
annualizedReturn: number;
sharpeRatio: number;
maxDrawdown: number;
maxDrawdownDuration: number;
winRate: number;
totalTrades: number;
winningTrades: number;
losingTrades: number;
averageWinningTrade: number;
averageLosingTrade: number;
profitFactor: number;
dailyReturns: Array<{ date: Date; return: number }>;
trades: Array<{
symbol: string;
entryTime: Date;
entryPrice: number;
exitTime: Date;
exitPrice: number;
quantity: number;
pnl: number;
pnlPercent: number;
}>;
// Advanced metrics
sortinoRatio?: number;
calmarRatio?: number;
omegaRatio?: number;
cagr?: number;
volatility?: number;
ulcerIndex?: number;
}
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
@Injectable({
providedIn: 'root'
})
export class StrategyService {
private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint
constructor(private http: HttpClient) { }
// Strategy Management
getStrategies(): Observable<ApiResponse<TradingStrategy[]>> {
return this.http.get<ApiResponse<TradingStrategy[]>>(`${this.apiBaseUrl}/strategies`);
}
getStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.get<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`);
}
createStrategy(strategy: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy);
}
updateStrategy(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>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/start`, {});
}
stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/stop`, {});
}
pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/pause`, {});
}
// Backtest Management
getStrategyTypes(): Observable<ApiResponse<string[]>> {
return this.http.get<ApiResponse<string[]>>(`${this.apiBaseUrl}/strategy-types`);
}
getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> {
return this.http.get<ApiResponse<Record<string, any>>>(`${this.apiBaseUrl}/strategy-parameters/${type}`);
}
runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> {
return this.http.post<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest`, request);
}
getBacktestResult(id: string): Observable<ApiResponse<BacktestResult>> {
return this.http.get<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest/${id}`);
}
optimizeStrategy(
baseRequest: BacktestRequest,
parameterGrid: Record<string, any[]>
): Observable<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>> {
return this.http.post<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>>(
`${this.apiBaseUrl}/backtest/optimize`,
{ baseRequest, parameterGrid }
);
}
// Strategy Signals and Trades
getStrategySignals(strategyId: string): Observable<ApiResponse<Array<{
id: string;
strategyId: string;
symbol: string;
action: string;
price: number;
quantity: number;
timestamp: Date;
confidence: number;
metadata?: any;
}>>> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`);
}
getStrategyTrades(strategyId: string): Observable<ApiResponse<Array<{
id: string;
strategyId: string;
symbol: string;
entryPrice: number;
entryTime: Date;
exitPrice: number;
exitTime: Date;
quantity: number;
pnl: number;
pnlPercent: number;
}>>> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`);
}
// Helper methods for common transformations
formatBacktestRequest(formData: any): BacktestRequest {
// Handle date formatting and parameter conversion
return {
...formData,
startDate: formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate,
endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate,
strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams)
};
}
private convertParameterTypes(strategyType: string, params: Record<string, any>): Record<string, any> {
// Convert string parameters to correct types based on strategy requirements
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'string') {
// Try to convert to number if it looks like a number
if (!isNaN(Number(value))) {
result[key] = Number(value);
} else if (value.toLowerCase() === 'true') {
result[key] = true;
} else if (value.toLowerCase() === 'false') {
result[key] = false;
} else {
result[key] = value;
}
} else {
result[key] = value;
}
}
return result;
}
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface TradingStrategy {
id: string;
name: string;
description: string;
status: 'ACTIVE' | 'INACTIVE' | 'PAUSED' | 'ERROR';
type: string;
symbols: string[];
parameters: Record<string, any>;
performance: {
totalTrades: number;
winRate: number;
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
};
createdAt: Date;
updatedAt: Date;
}
export interface BacktestRequest {
strategyType: string;
strategyParams: Record<string, any>;
symbols: string[];
startDate: Date | string;
endDate: Date | string;
initialCapital: number;
dataResolution: '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
commission: number;
slippage: number;
mode: 'event' | 'vector';
}
export interface BacktestResult {
strategyId: string;
startDate: Date;
endDate: Date;
duration: number;
initialCapital: number;
finalCapital: number;
totalReturn: number;
annualizedReturn: number;
sharpeRatio: number;
maxDrawdown: number;
maxDrawdownDuration: number;
winRate: number;
totalTrades: number;
winningTrades: number;
losingTrades: number;
averageWinningTrade: number;
averageLosingTrade: number;
profitFactor: number;
dailyReturns: Array<{ date: Date; return: number }>;
trades: Array<{
symbol: string;
entryTime: Date;
entryPrice: number;
exitTime: Date;
exitPrice: number;
quantity: number;
pnl: number;
pnlPercent: number;
}>;
// Advanced metrics
sortinoRatio?: number;
calmarRatio?: number;
omegaRatio?: number;
cagr?: number;
volatility?: number;
ulcerIndex?: number;
}
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
@Injectable({
providedIn: 'root',
})
export class StrategyService {
private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint
constructor(private http: HttpClient) {}
// Strategy Management
getStrategies(): Observable<ApiResponse<TradingStrategy[]>> {
return this.http.get<ApiResponse<TradingStrategy[]>>(`${this.apiBaseUrl}/strategies`);
}
getStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.get<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`);
}
createStrategy(strategy: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy);
}
updateStrategy(
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>> {
return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/start`,
{}
);
}
stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/stop`,
{}
);
}
pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(
`${this.apiBaseUrl}/strategies/${id}/pause`,
{}
);
}
// Backtest Management
getStrategyTypes(): Observable<ApiResponse<string[]>> {
return this.http.get<ApiResponse<string[]>>(`${this.apiBaseUrl}/strategy-types`);
}
getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> {
return this.http.get<ApiResponse<Record<string, any>>>(
`${this.apiBaseUrl}/strategy-parameters/${type}`
);
}
runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> {
return this.http.post<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest`, request);
}
getBacktestResult(id: string): Observable<ApiResponse<BacktestResult>> {
return this.http.get<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest/${id}`);
}
optimizeStrategy(
baseRequest: BacktestRequest,
parameterGrid: Record<string, any[]>
): Observable<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>> {
return this.http.post<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>>(
`${this.apiBaseUrl}/backtest/optimize`,
{ baseRequest, parameterGrid }
);
}
// Strategy Signals and Trades
getStrategySignals(strategyId: string): Observable<
ApiResponse<
Array<{
id: string;
strategyId: string;
symbol: string;
action: string;
price: number;
quantity: number;
timestamp: Date;
confidence: number;
metadata?: any;
}>
>
> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`);
}
getStrategyTrades(strategyId: string): Observable<
ApiResponse<
Array<{
id: string;
strategyId: string;
symbol: string;
entryPrice: number;
entryTime: Date;
exitPrice: number;
exitTime: Date;
quantity: number;
pnl: number;
pnlPercent: number;
}>
>
> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`);
}
// Helper methods for common transformations
formatBacktestRequest(formData: any): BacktestRequest {
// Handle date formatting and parameter conversion
return {
...formData,
startDate:
formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate,
endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate,
strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams),
};
}
private convertParameterTypes(
strategyType: string,
params: Record<string, any>
): Record<string, any> {
// Convert string parameters to correct types based on strategy requirements
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'string') {
// Try to convert to number if it looks like a number
if (!isNaN(Number(value))) {
result[key] = Number(value);
} else if (value.toLowerCase() === 'true') {
result[key] = true;
} else if (value.toLowerCase() === 'false') {
result[key] = false;
} else {
result[key] = value;
}
} else {
result[key] = value;
}
}
return result;
}
}

View file

@ -1,218 +1,215 @@
import { Injectable, signal } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
export interface WebSocketMessage {
type: string;
data: any;
timestamp: string;
}
export interface MarketDataUpdate {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
export interface RiskAlert {
id: string;
symbol: string;
alertType: 'POSITION_LIMIT' | 'DAILY_LOSS' | 'VOLATILITY' | 'PORTFOLIO_RISK';
message: string;
severity: 'LOW' | 'MEDIUM' | 'HIGH';
timestamp: string;
}
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
private readonly WS_ENDPOINTS = {
marketData: 'ws://localhost:3001/ws',
riskGuardian: 'ws://localhost:3002/ws',
strategyOrchestrator: 'ws://localhost:3003/ws'
};
private connections = new Map<string, WebSocket>();
private messageSubjects = new Map<string, Subject<WebSocketMessage>>();
// Connection status signals
public isConnected = signal<boolean>(false);
public connectionStatus = signal<{ [key: string]: boolean }>({
marketData: false,
riskGuardian: false,
strategyOrchestrator: false
});
constructor() {
this.initializeConnections();
}
private initializeConnections() {
// Initialize WebSocket connections for all services
Object.entries(this.WS_ENDPOINTS).forEach(([service, url]) => {
this.connect(service, url);
});
}
private connect(serviceName: string, url: string) {
try {
const ws = new WebSocket(url);
const messageSubject = new Subject<WebSocketMessage>();
ws.onopen = () => {
console.log(`Connected to ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, true);
};
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
messageSubject.next(message);
} catch (error) {
console.error(`Failed to parse WebSocket message from ${serviceName}:`, error);
}
};
ws.onclose = () => {
console.log(`Disconnected from ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, false);
// Attempt to reconnect after 5 seconds
setTimeout(() => {
this.connect(serviceName, url);
}, 5000);
};
ws.onerror = (error) => {
console.error(`WebSocket error for ${serviceName}:`, error);
this.updateConnectionStatus(serviceName, false);
};
this.connections.set(serviceName, ws);
this.messageSubjects.set(serviceName, messageSubject);
} catch (error) {
console.error(`Failed to connect to ${serviceName} WebSocket:`, error);
this.updateConnectionStatus(serviceName, false);
}
}
private updateConnectionStatus(serviceName: string, isConnected: boolean) {
const currentStatus = this.connectionStatus();
const newStatus = { ...currentStatus, [serviceName]: isConnected };
this.connectionStatus.set(newStatus);
// Update overall connection status
const overallConnected = Object.values(newStatus).some(status => status);
this.isConnected.set(overallConnected);
}
// Market Data Updates
getMarketDataUpdates(): Observable<MarketDataUpdate> {
const subject = this.messageSubjects.get('marketData');
if (!subject) {
throw new Error('Market data WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'market_data_update'),
map(message => message.data as MarketDataUpdate)
);
}
// Risk Alerts
getRiskAlerts(): Observable<RiskAlert> {
const subject = this.messageSubjects.get('riskGuardian');
if (!subject) {
throw new Error('Risk Guardian WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'risk_alert'),
map(message => message.data as RiskAlert)
);
}
// Strategy Updates
getStrategyUpdates(): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'strategy_update'),
map(message => message.data)
);
}
// Strategy Signals
getStrategySignals(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message =>
message.type === 'strategy_signal' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// Strategy Trades
getStrategyTrades(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message =>
message.type === 'strategy_trade' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// All strategy-related messages, useful for components that need all types
getAllStrategyMessages(): Observable<WebSocketMessage> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message =>
message.type.startsWith('strategy_')
)
);
}
// Send messages
sendMessage(serviceName: string, message: any) {
const ws = this.connections.get(serviceName);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
} else {
console.warn(`Cannot send message to ${serviceName}: WebSocket not connected`);
}
}
// Cleanup
disconnect() {
this.connections.forEach((ws, serviceName) => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
this.connections.clear();
this.messageSubjects.clear();
}
}
import { Injectable, signal } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
export interface WebSocketMessage {
type: string;
data: any;
timestamp: string;
}
export interface MarketDataUpdate {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
export interface RiskAlert {
id: string;
symbol: string;
alertType: 'POSITION_LIMIT' | 'DAILY_LOSS' | 'VOLATILITY' | 'PORTFOLIO_RISK';
message: string;
severity: 'LOW' | 'MEDIUM' | 'HIGH';
timestamp: string;
}
@Injectable({
providedIn: 'root',
})
export class WebSocketService {
private readonly WS_ENDPOINTS = {
marketData: 'ws://localhost:3001/ws',
riskGuardian: 'ws://localhost:3002/ws',
strategyOrchestrator: 'ws://localhost:3003/ws',
};
private connections = new Map<string, WebSocket>();
private messageSubjects = new Map<string, Subject<WebSocketMessage>>();
// Connection status signals
public isConnected = signal<boolean>(false);
public connectionStatus = signal<{ [key: string]: boolean }>({
marketData: false,
riskGuardian: false,
strategyOrchestrator: false,
});
constructor() {
this.initializeConnections();
}
private initializeConnections() {
// Initialize WebSocket connections for all services
Object.entries(this.WS_ENDPOINTS).forEach(([service, url]) => {
this.connect(service, url);
});
}
private connect(serviceName: string, url: string) {
try {
const ws = new WebSocket(url);
const messageSubject = new Subject<WebSocketMessage>();
ws.onopen = () => {
console.log(`Connected to ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, true);
};
ws.onmessage = event => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
messageSubject.next(message);
} catch (error) {
console.error(`Failed to parse WebSocket message from ${serviceName}:`, error);
}
};
ws.onclose = () => {
console.log(`Disconnected from ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, false);
// Attempt to reconnect after 5 seconds
setTimeout(() => {
this.connect(serviceName, url);
}, 5000);
};
ws.onerror = error => {
console.error(`WebSocket error for ${serviceName}:`, error);
this.updateConnectionStatus(serviceName, false);
};
this.connections.set(serviceName, ws);
this.messageSubjects.set(serviceName, messageSubject);
} catch (error) {
console.error(`Failed to connect to ${serviceName} WebSocket:`, error);
this.updateConnectionStatus(serviceName, false);
}
}
private updateConnectionStatus(serviceName: string, isConnected: boolean) {
const currentStatus = this.connectionStatus();
const newStatus = { ...currentStatus, [serviceName]: isConnected };
this.connectionStatus.set(newStatus);
// Update overall connection status
const overallConnected = Object.values(newStatus).some(status => status);
this.isConnected.set(overallConnected);
}
// Market Data Updates
getMarketDataUpdates(): Observable<MarketDataUpdate> {
const subject = this.messageSubjects.get('marketData');
if (!subject) {
throw new Error('Market data WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'market_data_update'),
map(message => message.data as MarketDataUpdate)
);
}
// Risk Alerts
getRiskAlerts(): Observable<RiskAlert> {
const subject = this.messageSubjects.get('riskGuardian');
if (!subject) {
throw new Error('Risk Guardian WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'risk_alert'),
map(message => message.data as RiskAlert)
);
}
// Strategy Updates
getStrategyUpdates(): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'strategy_update'),
map(message => message.data)
);
}
// Strategy Signals
getStrategySignals(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(
message =>
message.type === 'strategy_signal' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// Strategy Trades
getStrategyTrades(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(
message =>
message.type === 'strategy_trade' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// All strategy-related messages, useful for components that need all types
getAllStrategyMessages(): Observable<WebSocketMessage> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(filter(message => message.type.startsWith('strategy_')));
}
// Send messages
sendMessage(serviceName: string, message: any) {
const ws = this.connections.get(serviceName);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
} else {
console.warn(`Cannot send message to ${serviceName}: WebSocket not connected`);
}
}
// Cleanup
disconnect() {
this.connections.forEach((ws, serviceName) => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
this.connections.clear();
this.messageSubjects.clear();
}
}

View file

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