198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
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);
|
|
}
|
|
}
|