381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
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.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;
|
|
}
|
|
}
|