fixed up dashboard

This commit is contained in:
Bojan Kucera 2025-06-02 20:44:38 -04:00
parent 90168ba619
commit e3bfd05b90
17 changed files with 1522 additions and 92 deletions

View file

@ -1,12 +1,14 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="space-y-6"> <!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Market Data</h1>
<p class="text-gray-600 mt-1">Real-time market information and analytics</p>
</div>
<button mat-raised-button color="primary">
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
@ -57,10 +59,24 @@
Export
</button>
</div>
</div>
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading market data...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
@ -111,12 +127,11 @@
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
${{ stock.marketCap }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
</ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</div>
}
</mat-card>
<!-- Market Analytics Tabs -->

View file

@ -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<ExtendedMarketData[]>([
{
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<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'];
protected currentTime = signal<string>(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);
}
}

View file

@ -1,15 +1,203 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Portfolio</h1>
<p class="text-gray-600 mt-1">Manage and monitor your investment portfolio</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">account_balance_wallet</mat-icon>
<p class="mb-4">Portfolio management features will be implemented here</p>
</div>
</mat-card>
<!-- Portfolio Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total Value</p>
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().totalValue.toLocaleString() }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total P&L</p>
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().totalPnL)">
{{ portfolioSummary().totalPnL > 0 ? '+' : '' }}${{ portfolioSummary().totalPnL.toLocaleString() }}
({{ portfolioSummary().totalPnLPercent.toFixed(2) }}%)
</p>
</div>
<mat-icon class="text-green-600 text-3xl" *ngIf="portfolioSummary().totalPnL >= 0">trending_up</mat-icon>
<mat-icon class="text-red-600 text-3xl" *ngIf="portfolioSummary().totalPnL < 0">trending_down</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Day Change</p>
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().dayChange)">
{{ portfolioSummary().dayChange > 0 ? '+' : '' }}${{ portfolioSummary().dayChange.toLocaleString() }}
({{ portfolioSummary().dayChangePercent.toFixed(2) }}%)
</p>
</div>
<mat-icon class="text-purple-600 text-3xl">today</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Cash Available</p>
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().cash.toLocaleString() }}</p>
</div>
<mat-icon class="text-yellow-600 text-3xl">attach_money</mat-icon>
</div>
</mat-card>
</div>
<!-- Portfolio Tabs -->
<mat-tab-group>
<mat-tab label="Positions">
<div class="p-6">
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Current Positions</h3>
<div class="flex gap-2">
<button mat-button>
<mat-icon>add</mat-icon>
Add Position
</button>
<button mat-button>
<mat-icon>file_download</mat-icon>
Export
</button>
</div>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading portfolio...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else if (positions().length === 0) {
<div class="text-center py-8 text-gray-500">
<mat-icon class="text-4xl">account_balance_wallet</mat-icon>
<p class="mt-2">No positions found</p>
<button mat-button color="primary" class="mt-2">
<mat-icon>add</mat-icon>
Add Your First Position
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="positions()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let position" class="font-semibold text-gray-900">{{ position.symbol }}</td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Quantity</th>
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
{{ position.quantity.toLocaleString() }}
</td>
</ng-container>
<!-- Average Price Column -->
<ng-container matColumnDef="avgPrice">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Avg Price</th>
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
${{ position.avgPrice.toFixed(2) }}
</td>
</ng-container>
<!-- Current Price Column -->
<ng-container matColumnDef="currentPrice">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Current Price</th>
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
${{ position.currentPrice.toFixed(2) }}
</td>
</ng-container>
<!-- Market Value Column -->
<ng-container matColumnDef="marketValue">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Value</th>
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
${{ position.marketValue.toLocaleString() }}
</td>
</ng-container>
<!-- Unrealized P&L Column -->
<ng-container matColumnDef="unrealizedPnL">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Unrealized P&L</th>
<td mat-cell *matCellDef="let position"
class="text-right font-medium"
[class]="getPnLColor(position.unrealizedPnL)">
{{ position.unrealizedPnL > 0 ? '+' : '' }}${{ position.unrealizedPnL.toLocaleString() }}
({{ position.unrealizedPnLPercent.toFixed(2) }}%)
</td>
</ng-container>
<!-- Day Change Column -->
<ng-container matColumnDef="dayChange">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Day Change</th>
<td mat-cell *matCellDef="let position"
class="text-right font-medium"
[class]="getPnLColor(position.dayChange)">
{{ position.dayChange > 0 ? '+' : '' }}${{ position.dayChange.toFixed(2) }}
({{ position.dayChangePercent.toFixed(2) }}%)
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
</div>
</mat-tab>
<mat-tab label="Performance">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">trending_up</mat-icon>
<p class="mb-4">Performance charts and analytics will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Orders">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt</mat-icon>
<p class="mb-4">Order history and management will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>
</div>

View file

@ -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<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';
}
}

View file

@ -1,15 +1,178 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Risk Management</h1>
<p class="text-gray-600 mt-1">Monitor and control trading risks and exposure</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">security</mat-icon>
<p class="mb-4">Risk management tools and monitoring will be implemented here</p>
<!-- Risk Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
@if (riskThresholds(); as thresholds) {
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Max Position Size</p>
<p class="text-lg font-semibold text-gray-900">${{ thresholds.maxPositionSize.toLocaleString() }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Max Daily Loss</p>
<p class="text-lg font-semibold text-red-600">${{ thresholds.maxDailyLoss.toLocaleString() }}</p>
</div>
<mat-icon class="text-red-600 text-3xl">trending_down</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Portfolio Risk Limit</p>
<p class="text-lg font-semibold text-yellow-600">{{ (thresholds.maxPortfolioRisk * 100).toFixed(1) }}%</p>
</div>
<mat-icon class="text-yellow-600 text-3xl">pie_chart</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Volatility Limit</p>
<p class="text-lg font-semibold text-purple-600">{{ (thresholds.volatilityLimit * 100).toFixed(1) }}%</p>
</div>
<mat-icon class="text-purple-600 text-3xl">show_chart</mat-icon>
</div>
</mat-card>
}
</div>
<!-- Risk Thresholds Configuration -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Risk Thresholds Configuration</h3>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading risk settings...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<form [formGroup]="thresholdsForm" (ngSubmit)="saveThresholds()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<mat-form-field appearance="outline">
<mat-label>Max Position Size ($)</mat-label>
<input matInput type="number" formControlName="maxPositionSize" placeholder="100000">
<mat-icon matSuffix>attach_money</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Daily Loss ($)</mat-label>
<input matInput type="number" formControlName="maxDailyLoss" placeholder="5000">
<mat-icon matSuffix>trending_down</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Portfolio Risk (0-1)</mat-label>
<input matInput type="number" step="0.01" formControlName="maxPortfolioRisk" placeholder="0.1">
<mat-icon matSuffix>pie_chart</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Volatility Limit (0-1)</mat-label>
<input matInput type="number" step="0.01" formControlName="volatilityLimit" placeholder="0.3">
<mat-icon matSuffix>show_chart</mat-icon>
</mat-form-field>
</div>
<div class="flex justify-end mt-4">
<button mat-raised-button color="primary" type="submit" [disabled]="thresholdsForm.invalid || isSaving()">
@if (isSaving()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Save Thresholds
</button>
</div>
</form>
}
</mat-card>
<!-- Risk History Table -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Risk Evaluations</h3>
</div>
@if (riskHistory().length === 0) {
<div class="text-center py-8 text-gray-500">
<mat-icon class="text-4xl">history</mat-icon>
<p class="mt-2">No risk evaluations found</p>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="riskHistory()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let risk" class="font-semibold text-gray-900">{{ risk.symbol }}</td>
</ng-container>
<!-- Position Value Column -->
<ng-container matColumnDef="positionValue">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Position Value</th>
<td mat-cell *matCellDef="let risk" class="text-right font-medium text-gray-900">
${{ risk.positionValue.toLocaleString() }}
</td>
</ng-container>
<!-- Risk Level Column -->
<ng-container matColumnDef="riskLevel">
<th mat-header-cell *matHeaderCellDef class="text-center font-medium text-gray-900">Risk Level</th>
<td mat-cell *matCellDef="let risk" class="text-center">
<span class="px-2 py-1 rounded-full text-sm font-medium" [class]="getRiskLevelColor(risk.riskLevel)">
{{ risk.riskLevel }}
</span>
</td>
</ng-container>
<!-- Violations Column -->
<ng-container matColumnDef="violations">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Violations</th>
<td mat-cell *matCellDef="let risk" class="text-gray-600">
@if (risk.violations.length > 0) {
<span class="text-red-600">{{ risk.violations.join(', ') }}</span>
} @else {
<span class="text-green-600">None</span>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
</div>

View file

@ -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<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';
}
}
}