linxus fs fixes

This commit is contained in:
Boki 2025-06-09 22:55:51 -04:00
parent ac23b70146
commit 0b7846fe67
292 changed files with 41947 additions and 41947 deletions

View file

@ -1,165 +1,165 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-drawdown-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="drawdown-chart-container">
<canvas #drawdownChart></canvas>
</div>
`,
styles: `
.drawdown-chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
`
})
export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Calculate drawdown series from daily returns
const drawdownData = this.calculateDrawdownSeries(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: drawdownData.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Drawdown',
data: drawdownData.drawdowns,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.3,
borderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return (value * 100).toFixed(1) + '%';
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
},
min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += (context.parsed.y * 100).toFixed(2) + '%';
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateDrawdownSeries(result: BacktestResult): {
dates: Date[];
drawdowns: number[];
} {
const dates: Date[] = [];
const drawdowns: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate equity curve
let equity = 1;
const equityCurve: number[] = [];
for (const daily of sortedReturns) {
equity *= (1 + daily.return);
equityCurve.push(equity);
dates.push(new Date(daily.date));
}
// Calculate running maximum (high water mark)
let hwm = equityCurve[0];
for (let i = 0; i < equityCurve.length; i++) {
// Update high water mark
hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark
const drawdown = (equityCurve[i] / hwm) - 1;
drawdowns.push(drawdown);
}
return { dates, drawdowns };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-drawdown-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="drawdown-chart-container">
<canvas #drawdownChart></canvas>
</div>
`,
styles: `
.drawdown-chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
`
})
export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Calculate drawdown series from daily returns
const drawdownData = this.calculateDrawdownSeries(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: drawdownData.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Drawdown',
data: drawdownData.drawdowns,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.3,
borderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return (value * 100).toFixed(1) + '%';
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
},
min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += (context.parsed.y * 100).toFixed(2) + '%';
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateDrawdownSeries(result: BacktestResult): {
dates: Date[];
drawdowns: number[];
} {
const dates: Date[] = [];
const drawdowns: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate equity curve
let equity = 1;
const equityCurve: number[] = [];
for (const daily of sortedReturns) {
equity *= (1 + daily.return);
equityCurve.push(equity);
dates.push(new Date(daily.date));
}
// Calculate running maximum (high water mark)
let hwm = equityCurve[0];
for (let i = 0; i < equityCurve.length; i++) {
// Update high water mark
hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark
const drawdown = (equityCurve[i] / hwm) - 1;
drawdowns.push(drawdown);
}
return { dates, drawdowns };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}

View file

@ -1,171 +1,171 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-equity-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="equity-chart-container">
<canvas #equityChart></canvas>
</div>
`,
styles: `
.equity-chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
`
})
export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Prepare data
const equityCurve = this.calculateEquityCurve(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: equityCurve.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Portfolio Value',
data: equityCurve.values,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3,
borderWidth: 2,
fill: true
},
{
label: 'Benchmark',
data: equityCurve.benchmark,
borderColor: 'rgba(153, 102, 255, 0.5)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 1,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
}
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
.format(context.parsed.y);
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateEquityCurve(result: BacktestResult): {
dates: Date[];
values: number[];
benchmark: number[];
} {
const initialValue = result.initialCapital;
const dates: Date[] = [];
const values: number[] = [];
const benchmark: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate cumulative portfolio values
let portfolioValue = initialValue;
let benchmarkValue = initialValue;
for (const daily of sortedReturns) {
const date = new Date(daily.date);
portfolioValue = portfolioValue * (1 + daily.return);
// Simple benchmark (e.g., assuming 8% annualized return for a market index)
benchmarkValue = benchmarkValue * (1 + 0.08 / 365);
dates.push(date);
values.push(portfolioValue);
benchmark.push(benchmarkValue);
}
return { dates, values, benchmark };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-equity-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="equity-chart-container">
<canvas #equityChart></canvas>
</div>
`,
styles: `
.equity-chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
`
})
export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Prepare data
const equityCurve = this.calculateEquityCurve(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: equityCurve.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Portfolio Value',
data: equityCurve.values,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3,
borderWidth: 2,
fill: true
},
{
label: 'Benchmark',
data: equityCurve.benchmark,
borderColor: 'rgba(153, 102, 255, 0.5)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 1,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
}
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
.format(context.parsed.y);
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateEquityCurve(result: BacktestResult): {
dates: Date[];
values: number[];
benchmark: number[];
} {
const initialValue = result.initialCapital;
const dates: Date[] = [];
const values: number[] = [];
const benchmark: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate cumulative portfolio values
let portfolioValue = initialValue;
let benchmarkValue = initialValue;
for (const daily of sortedReturns) {
const date = new Date(daily.date);
portfolioValue = portfolioValue * (1 + daily.return);
// Simple benchmark (e.g., assuming 8% annualized return for a market index)
benchmarkValue = benchmarkValue * (1 + 0.08 / 365);
dates.push(date);
values.push(portfolioValue);
benchmark.push(benchmarkValue);
}
return { dates, values, benchmark };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}

View file

@ -1,258 +1,258 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatDividerModule } from '@angular/material/divider';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatGridListModule,
MatDividerModule,
MatTooltipModule
],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">Total Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.totalReturn || 0)">
{{formatPercent(backtestResult?.totalReturn || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized return (adjusted for the backtest duration)">Annualized Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)">
{{formatPercent(backtestResult?.annualizedReturn || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{formatPercent(backtestResult?.cagr || 0)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">Max Drawdown</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.maxDrawdown || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">Max DD Duration</div>
<div class="metric-value">
{{formatDays(backtestResult?.maxDrawdownDuration || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">Volatility</div>
<div class="metric-value">
{{formatPercent(backtestResult?.volatility || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Square root of the sum of the squares of drawdowns">Ulcer Index</div>
<div class="metric-value">
{{(backtestResult?.ulcerIndex || 0).toFixed(4)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">Sharpe Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)">
{{(backtestResult?.sharpeRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">Sortino Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)">
{{(backtestResult?.sortinoRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">Calmar Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)">
{{(backtestResult?.calmarRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Probability-weighted ratio of gains vs. losses">Omega Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)">
{{(backtestResult?.omegaRatio || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{backtestResult?.totalTrades || 0}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{formatPercent(backtestResult?.winRate || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{formatPercent(backtestResult?.averageWinningTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.averageLosingTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">Profit Factor</div>
<div class="metric-value" [ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)">
{{(backtestResult?.profitFactor || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4CAF50;
}
.negative {
color: #F44336;
}
.neutral {
color: #FFA000;
}
mat-divider {
margin: 8px 0;
}
`
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) return 'positive';
if (value < 0) return 'negative';
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
if (value < 0) return 'negative';
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) return 'positive';
if (value >= 0.45) return 'neutral';
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
return 'negative';
}
}
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatDividerModule } from '@angular/material/divider';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatGridListModule,
MatDividerModule,
MatTooltipModule
],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">Total Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.totalReturn || 0)">
{{formatPercent(backtestResult?.totalReturn || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized return (adjusted for the backtest duration)">Annualized Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)">
{{formatPercent(backtestResult?.annualizedReturn || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{formatPercent(backtestResult?.cagr || 0)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">Max Drawdown</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.maxDrawdown || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">Max DD Duration</div>
<div class="metric-value">
{{formatDays(backtestResult?.maxDrawdownDuration || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">Volatility</div>
<div class="metric-value">
{{formatPercent(backtestResult?.volatility || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Square root of the sum of the squares of drawdowns">Ulcer Index</div>
<div class="metric-value">
{{(backtestResult?.ulcerIndex || 0).toFixed(4)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">Sharpe Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)">
{{(backtestResult?.sharpeRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">Sortino Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)">
{{(backtestResult?.sortinoRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">Calmar Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)">
{{(backtestResult?.calmarRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Probability-weighted ratio of gains vs. losses">Omega Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)">
{{(backtestResult?.omegaRatio || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{backtestResult?.totalTrades || 0}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{formatPercent(backtestResult?.winRate || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{formatPercent(backtestResult?.averageWinningTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.averageLosingTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">Profit Factor</div>
<div class="metric-value" [ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)">
{{(backtestResult?.profitFactor || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4CAF50;
}
.negative {
color: #F44336;
}
.neutral {
color: #FFA000;
}
mat-divider {
margin: 8px 0;
}
`
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) return 'positive';
if (value < 0) return 'negative';
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
if (value < 0) return 'negative';
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) return 'positive';
if (value >= 0.45) return 'neutral';
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
return 'negative';
}
}

View file

@ -1,221 +1,221 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
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);
}
}
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
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);
}
}