stock-bot/apps/dashboard/src/app/pages/strategies/components/trades-table.component.ts

259 lines
7.5 KiB
TypeScript

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);
}
}