259 lines
7.5 KiB
TypeScript
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);
|
|
}
|
|
}
|