diff --git a/apps/intelligence-services/strategy-orchestrator/src/core/Strategy.ts b/apps/intelligence-services/strategy-orchestrator/src/core/Strategy.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/interface-services/trading-dashboard/src/app/app.config.ts b/apps/interface-services/trading-dashboard/src/app/app.config.ts index fcf580b..5f0a2ef 100644 --- a/apps/interface-services/trading-dashboard/src/app/app.config.ts +++ b/apps/interface-services/trading-dashboard/src/app/app.config.ts @@ -1,5 +1,6 @@ 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'; @@ -9,6 +10,7 @@ export const appConfig: ApplicationConfig = { provideBrowserGlobalErrorListeners(), provideZonelessChangeDetection(), provideRouter(routes), + provideHttpClient(), provideAnimationsAsync() ] }; diff --git a/apps/interface-services/trading-dashboard/src/app/app.html b/apps/interface-services/trading-dashboard/src/app/app.html index 953d59d..5ee5ca3 100644 --- a/apps/interface-services/trading-dashboard/src/app/app.html +++ b/apps/interface-services/trading-dashboard/src/app/app.html @@ -9,12 +9,9 @@ - {{ title }} + {{ title }} - + diff --git a/apps/interface-services/trading-dashboard/src/app/app.ts b/apps/interface-services/trading-dashboard/src/app/app.ts index fbe7d80..8e869c2 100644 --- a/apps/interface-services/trading-dashboard/src/app/app.ts +++ b/apps/interface-services/trading-dashboard/src/app/app.ts @@ -7,6 +7,7 @@ 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', @@ -18,7 +19,8 @@ import { SidebarComponent } from './components/sidebar/sidebar.component'; MatButtonModule, MatIconModule, MatChipsModule, - SidebarComponent + SidebarComponent, + NotificationsComponent ], templateUrl: './app.html', styleUrl: './app.css' diff --git a/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.css b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.css new file mode 100644 index 0000000..c16e6e1 --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.css @@ -0,0 +1,45 @@ +::ng-deep .notification-menu { + width: 380px; + max-width: 90vw; +} + +.notification-header { + padding: 12px 16px !important; + height: auto !important; + line-height: normal !important; +} + +.notification-empty { + padding: 16px !important; + height: auto !important; + line-height: normal !important; +} + +.notification-item { + padding: 12px 16px !important; + height: auto !important; + line-height: normal !important; + white-space: normal !important; + border-left: 3px solid transparent; + transition: all 0.2s ease; +} + +.notification-item:hover { + background-color: #f5f5f5; +} + +.notification-item.unread { + background-color: #f0f9ff; + border-left-color: #0ea5e9; +} + +.notification-item.unread .font-medium { + font-weight: 600; +} + +.line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} \ No newline at end of file diff --git a/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.html b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.html new file mode 100644 index 0000000..e85f512 --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.html @@ -0,0 +1,75 @@ + + + +
+
+ Notifications + @if (notifications.length > 0) { +
+ + +
+ } +
+
+ + + + @if (notifications.length === 0) { +
+
+ notifications_none +

No notifications

+
+
+ } @else { + @for (notification of notifications.slice(0, 5); track notification.id) { +
+
+ + {{ getNotificationIcon(notification.type) }} + +
+
+

{{ notification.title }}

+ +
+

{{ notification.message }}

+

{{ formatTime(notification.timestamp) }}

+
+
+
+ @if (!$last) { + + } + } + + @if (notifications.length > 5) { + +
+ {{ notifications.length - 5 }} more notifications... +
+ } + } +
diff --git a/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.spec.ts b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.spec.ts new file mode 100644 index 0000000..e51d2d9 --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Notifications } from './notifications'; + +describe('Notifications', () => { + let component: Notifications; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Notifications] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Notifications); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.ts b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.ts new file mode 100644 index 0000000..36ce2db --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/components/notifications/notifications.ts @@ -0,0 +1,86 @@ +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`; + } +} diff --git a/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.html b/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.html index 1dbfcae..9b35668 100644 --- a/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.html +++ b/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.html @@ -1,12 +1,14 @@ -
- +

Market Data

Real-time market information and analytics

-
@@ -57,10 +59,24 @@ Export
-
- -
- + + @if (isLoading()) { +
+ + Loading market data... +
+ } @else if (error()) { +
+ error +

{{ error() }}

+ +
+ } @else { +
+
@@ -111,12 +127,11 @@ - - - +
Symbol ${{ stock.marketCap }}
-
+ + } diff --git a/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.ts b/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.ts index e908c23..e4c8ee2 100644 --- a/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.ts +++ b/apps/interface-services/trading-dashboard/src/app/pages/market-data/market-data.component.ts @@ -1,10 +1,15 @@ -import { Component, signal } from '@angular/core'; +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; @@ -26,71 +31,168 @@ export interface ExtendedMarketData { MatButtonModule, MatIconModule, MatTableModule, - MatTabsModule + MatTabsModule, + MatProgressSpinnerModule, + MatSnackBarModule ], templateUrl: './market-data.component.html', styleUrl: './market-data.component.css' }) -export class MarketDataComponent { - protected marketData = signal([ - { - 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 - } - ]); +export class MarketDataComponent implements OnInit, OnDestroy { + private apiService = inject(ApiService); + private webSocketService = inject(WebSocketService); + private snackBar = inject(MatSnackBar); + private subscriptions: Subscription[] = []; + protected marketData = signal([]); + protected currentTime = signal(new Date().toLocaleTimeString()); + protected isLoading = signal(true); + protected error = signal(null); protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent', 'volume', 'marketCap']; - - protected currentTime = signal(new Date().toLocaleTimeString()); constructor() { + ngOnInit() { // Update time every second - setInterval(() => { + const timeSubscription = interval(1000).subscribe(() => { this.currentTime.set(new Date().toLocaleTimeString()); - }, 1000); + }); + 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); } } diff --git a/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.html b/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.html index 71b66ad..f62d8ac 100644 --- a/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.html +++ b/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.html @@ -1,15 +1,203 @@
+

Portfolio

Manage and monitor your investment portfolio

+
- -
- account_balance_wallet -

Portfolio management features will be implemented here

-
-
+ +
+ +
+
+

Total Value

+

${{ portfolioSummary().totalValue.toLocaleString() }}

+
+ account_balance_wallet +
+
+ + +
+
+

Total P&L

+

+ {{ portfolioSummary().totalPnL > 0 ? '+' : '' }}${{ portfolioSummary().totalPnL.toLocaleString() }} + ({{ portfolioSummary().totalPnLPercent.toFixed(2) }}%) +

+
+ trending_up + trending_down +
+
+ + +
+
+

Day Change

+

+ {{ portfolioSummary().dayChange > 0 ? '+' : '' }}${{ portfolioSummary().dayChange.toLocaleString() }} + ({{ portfolioSummary().dayChangePercent.toFixed(2) }}%) +

+
+ today +
+
+ + +
+
+

Cash Available

+

${{ portfolioSummary().cash.toLocaleString() }}

+
+ attach_money +
+
+
+ + + + +
+ +
+

Current Positions

+
+ + +
+
+ + @if (isLoading()) { +
+ + Loading portfolio... +
+ } @else if (error()) { +
+ error +

{{ error() }}

+ +
+ } @else if (positions().length === 0) { +
+ account_balance_wallet +

No positions found

+ +
+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Symbol{{ position.symbol }}Quantity + {{ position.quantity.toLocaleString() }} + Avg Price + ${{ position.avgPrice.toFixed(2) }} + Current Price + ${{ position.currentPrice.toFixed(2) }} + Market Value + ${{ position.marketValue.toLocaleString() }} + Unrealized P&L + {{ position.unrealizedPnL > 0 ? '+' : '' }}${{ position.unrealizedPnL.toLocaleString() }} + ({{ position.unrealizedPnLPercent.toFixed(2) }}%) + Day Change + {{ position.dayChange > 0 ? '+' : '' }}${{ position.dayChange.toFixed(2) }} + ({{ position.dayChangePercent.toFixed(2) }}%) +
+
+ } +
+
+
+ + +
+ +
+ trending_up +

Performance charts and analytics will be implemented here

+
+
+
+
+ + +
+ +
+ receipt +

Order history and management will be implemented here

+
+
+
+
+
diff --git a/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.ts b/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.ts index b2bcb67..8711236 100644 --- a/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.ts +++ b/apps/interface-services/trading-dashboard/src/app/pages/portfolio/portfolio.component.ts @@ -1,13 +1,159 @@ -import { Component } from '@angular/core'; +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], + imports: [ + CommonModule, + MatCardModule, + MatIconModule, + MatButtonModule, + MatTableModule, + MatProgressSpinnerModule, + MatSnackBarModule, + MatTabsModule + ], templateUrl: './portfolio.component.html', styleUrl: './portfolio.component.css' }) -export class PortfolioComponent {} +export class PortfolioComponent implements OnInit, OnDestroy { + private apiService = inject(ApiService); + private snackBar = inject(MatSnackBar); + private subscriptions: Subscription[] = []; + + protected portfolioSummary = signal({ + totalValue: 0, + totalCost: 0, + totalPnL: 0, + totalPnLPercent: 0, + dayChange: 0, + dayChangePercent: 0, + cash: 0, + positionsCount: 0 + }); + + protected positions = signal([]); + protected isLoading = signal(true); + protected error = signal(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'; + } +} diff --git a/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.html b/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.html index f34e299..59d88e0 100644 --- a/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.html +++ b/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.html @@ -1,15 +1,178 @@
+

Risk Management

Monitor and control trading risks and exposure

+
- -
- security -

Risk management tools and monitoring will be implemented here

+ +
+ @if (riskThresholds(); as thresholds) { + +
+
+

Max Position Size

+

${{ thresholds.maxPositionSize.toLocaleString() }}

+
+ account_balance +
+
+ + +
+
+

Max Daily Loss

+

${{ thresholds.maxDailyLoss.toLocaleString() }}

+
+ trending_down +
+
+ + +
+
+

Portfolio Risk Limit

+

{{ (thresholds.maxPortfolioRisk * 100).toFixed(1) }}%

+
+ pie_chart +
+
+ + +
+
+

Volatility Limit

+

{{ (thresholds.volatilityLimit * 100).toFixed(1) }}%

+
+ show_chart +
+
+ } +
+ + + +
+

Risk Thresholds Configuration

+ + @if (isLoading()) { +
+ + Loading risk settings... +
+ } @else if (error()) { +
+ error +

{{ error() }}

+ +
+ } @else { +
+
+ + Max Position Size ($) + + attach_money + + + + Max Daily Loss ($) + + trending_down + + + + Max Portfolio Risk (0-1) + + pie_chart + + + + Volatility Limit (0-1) + + show_chart + +
+ +
+ +
+
+ } +
+ + + +
+

Recent Risk Evaluations

+
+ + @if (riskHistory().length === 0) { +
+ history +

No risk evaluations found

+
+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Symbol{{ risk.symbol }}Position Value + ${{ risk.positionValue.toLocaleString() }} + Risk Level + + {{ risk.riskLevel }} + + Violations + @if (risk.violations.length > 0) { + {{ risk.violations.join(', ') }} + } @else { + None + } +
+
+ }
diff --git a/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.ts b/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.ts index de584b7..041df3c 100644 --- a/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.ts +++ b/apps/interface-services/trading-dashboard/src/app/pages/risk-management/risk-management.component.ts @@ -1,13 +1,135 @@ -import { Component } from '@angular/core'; +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], + imports: [ + CommonModule, + MatCardModule, + MatIconModule, + MatButtonModule, + MatTableModule, + MatFormFieldModule, + MatInputModule, + MatSnackBarModule, + MatProgressSpinnerModule, + ReactiveFormsModule + ], templateUrl: './risk-management.component.html', styleUrl: './risk-management.component.css' }) -export class RiskManagementComponent {} +export class RiskManagementComponent implements OnInit, OnDestroy { + private apiService = inject(ApiService); + private snackBar = inject(MatSnackBar); + private fb = inject(FormBuilder); + private subscriptions: Subscription[] = []; + + protected riskThresholds = signal(null); + protected riskHistory = signal([]); + protected isLoading = signal(true); + protected isSaving = signal(false); + protected error = signal(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'; + } + } +} diff --git a/apps/interface-services/trading-dashboard/src/app/services/api.service.ts b/apps/interface-services/trading-dashboard/src/app/services/api.service.ts new file mode 100644 index 0000000..e6ab258 --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/services/api.service.ts @@ -0,0 +1,98 @@ +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 { + return this.http.get(`${this.baseUrls[service]}/health`); + } +} diff --git a/apps/interface-services/trading-dashboard/src/app/services/notification.service.ts b/apps/interface-services/trading-dashboard/src/app/services/notification.service.ts new file mode 100644 index 0000000..9e1a1ef --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/services/notification.service.ts @@ -0,0 +1,193 @@ +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([]); + public unreadCount = signal(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(); + } +} diff --git a/apps/interface-services/trading-dashboard/src/app/services/websocket.service.ts b/apps/interface-services/trading-dashboard/src/app/services/websocket.service.ts new file mode 100644 index 0000000..866b5c7 --- /dev/null +++ b/apps/interface-services/trading-dashboard/src/app/services/websocket.service.ts @@ -0,0 +1,173 @@ +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(); + private messageSubjects = new Map>(); + + // Connection status signals + public isConnected = signal(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(); + + 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 { + 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 { + 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 { + 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) + ); + } + + // 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(); + } +}