dashboard cleanup
This commit is contained in:
parent
660a2a1ec2
commit
56e3938561
36 changed files with 1002 additions and 3481 deletions
|
|
@ -2,7 +2,8 @@
|
|||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "npm"
|
||||
"packageManager": "npm",
|
||||
"analytics": false
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './pages/dashboard/dashboard.component';
|
||||
import { ExchangesComponent } from './pages/exchanges/exchanges.component';
|
||||
import { MarketDataComponent } from './pages/market-data/market-data.component';
|
||||
import { PortfolioComponent } from './pages/portfolio/portfolio.component';
|
||||
import { RiskManagementComponent } from './pages/risk-management/risk-management.component';
|
||||
import { SettingsComponent } from './pages/settings/settings.component';
|
||||
import { StrategiesComponent } from './pages/strategies/strategies.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'market-data', component: MarketDataComponent },
|
||||
{ path: 'portfolio', component: PortfolioComponent },
|
||||
{ path: 'strategies', component: StrategiesComponent },
|
||||
{ path: 'risk-management', component: RiskManagementComponent },
|
||||
{ path: 'exchanges', component: ExchangesComponent },
|
||||
|
||||
{ path: 'settings', component: SettingsComponent },
|
||||
{ path: '**', redirectTo: '/dashboard' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
|
||||
import { NotificationsComponent } from './components/notifications/notifications';
|
||||
import { SidebarComponent } from './components/sidebar/sidebar.component';
|
||||
import { BreadcrumbService } from './services/breadcrumb.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BreadcrumbItem, BreadcrumbService } from '../../services/breadcrumb.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-breadcrumb',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatIconModule],
|
||||
template: `
|
||||
<nav class="breadcrumb-nav" *ngIf="breadcrumbs$ | async as breadcrumbs">
|
||||
<ol class="breadcrumb-list">
|
||||
<li
|
||||
*ngFor="let item of breadcrumbs; let last = last"
|
||||
class="breadcrumb-item"
|
||||
[class.active]="item.isActive"
|
||||
>
|
||||
<a
|
||||
*ngIf="!item.isActive; else activeItem"
|
||||
[routerLink]="item.url"
|
||||
class="breadcrumb-link"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
<ng-template #activeItem>
|
||||
<span class="breadcrumb-active">{{ item.label }}</span>
|
||||
</ng-template>
|
||||
<mat-icon *ngIf="!last" class="breadcrumb-separator">chevron_right</mat-icon>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
`,
|
||||
styles: [`
|
||||
.breadcrumb-nav {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.breadcrumb-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.breadcrumb-active {
|
||||
color: #111827;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: #9ca3af;
|
||||
font-size: 1rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class BreadcrumbComponent implements OnInit {
|
||||
breadcrumbs$: Observable<BreadcrumbItem[]>;
|
||||
|
||||
constructor(
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
private router: Router
|
||||
) {
|
||||
this.breadcrumbs$ = this.breadcrumbService.breadcrumbs$;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Initialize breadcrumbs for current route
|
||||
this.breadcrumbService.breadcrumbs$.subscribe();
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ export class SidebarComponent {
|
|||
protected navigationItems: NavigationItem[] = [
|
||||
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard', active: true },
|
||||
{ label: 'Market Data', icon: 'trending_up', route: '/market-data' },
|
||||
{ label: 'Exchanges', icon: 'account_balance', route: '/exchanges' },
|
||||
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
|
||||
{ label: 'Strategies', icon: 'psychology', route: '/strategies' },
|
||||
{ label: 'Risk Management', icon: 'security', route: '/risk-management' },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 400px;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.dialog-description {
|
||||
color: #6b7280;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
padding: 1rem 0 0 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form field spacing */
|
||||
mat-form-field + mat-form-field {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Custom validation styles */
|
||||
.mat-mdc-form-field.mat-form-field-invalid .mat-mdc-text-field-wrapper {
|
||||
background-color: rgba(244, 67, 54, 0.04);
|
||||
}
|
||||
|
||||
/* Dialog title styling */
|
||||
h2[mat-dialog-title] {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<h2 mat-dialog-title>Add Source Mapping</h2>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-content">
|
||||
<p class="dialog-description">
|
||||
Add a source mapping for exchange: <strong>{{ data.masterExchangeId }}</strong>
|
||||
</p>
|
||||
|
||||
<!-- Source Selection -->
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Data Source</mat-label>
|
||||
<mat-select formControlName="sourceName" required>
|
||||
@for (source of availableSourcesFiltered; track source.value) {
|
||||
<mat-option [value]="source.value">{{ source.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-error>Please select a data source</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Custom Source Name (shown when 'custom' is selected) -->
|
||||
@if (isCustomSource) {
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Custom Source Name</mat-label>
|
||||
<input matInput formControlName="sourceName_custom" required />
|
||||
<mat-error>Please enter a custom source name</mat-error>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<!-- Source ID -->
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Source Exchange ID</mat-label>
|
||||
<input matInput formControlName="sourceId" required />
|
||||
<mat-hint>The unique identifier for this exchange in the source system</mat-hint>
|
||||
<mat-error>Please enter the source exchange ID</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Optional Name -->
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Display Name (Optional)</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
<mat-hint>Human-readable name for this exchange mapping</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Optional Description -->
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Description (Optional)</mat-label>
|
||||
<textarea matInput formControlName="description" rows="3"></textarea>
|
||||
<mat-hint>Additional details about this exchange mapping</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="!form.valid">
|
||||
Add Mapping
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
export interface AddMappingDialogData {
|
||||
masterExchangeId: string;
|
||||
existingSources: string[];
|
||||
}
|
||||
|
||||
export interface AddMappingResult {
|
||||
sourceName: string;
|
||||
sourceData: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-mapping-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
],
|
||||
templateUrl: './add-mapping-dialog.component.html',
|
||||
styleUrl: './add-mapping-dialog.component.css',
|
||||
})
|
||||
export class AddMappingDialogComponent {
|
||||
form: FormGroup;
|
||||
availableSources = [
|
||||
{ value: 'ib', label: 'Interactive Brokers' },
|
||||
{ value: 'yahoo', label: 'Yahoo Finance' },
|
||||
{ value: 'alpha', label: 'Alpha Vantage' },
|
||||
{ value: 'polygon', label: 'Polygon.io' },
|
||||
{ value: 'finnhub', label: 'Finnhub' },
|
||||
{ value: 'custom', label: 'Custom Source' },
|
||||
];
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddMappingDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AddMappingDialogData
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
sourceName: ['', [Validators.required]],
|
||||
sourceId: ['', [Validators.required]],
|
||||
sourceName_custom: [''],
|
||||
name: [''],
|
||||
description: [''],
|
||||
});
|
||||
|
||||
// Add custom validator for custom source name
|
||||
this.form.get('sourceName')?.valueChanges.subscribe(value => {
|
||||
const customNameControl = this.form.get('sourceName_custom');
|
||||
if (value === 'custom') {
|
||||
customNameControl?.setValidators([Validators.required]);
|
||||
} else {
|
||||
customNameControl?.clearValidators();
|
||||
}
|
||||
customNameControl?.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
|
||||
get availableSourcesFiltered() {
|
||||
return this.availableSources.filter(
|
||||
source => !this.data.existingSources.includes(source.value)
|
||||
);
|
||||
}
|
||||
|
||||
get isCustomSource() {
|
||||
return this.form.get('sourceName')?.value === 'custom';
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.form.valid) {
|
||||
let sourceName = this.form.get('sourceName')?.value;
|
||||
|
||||
// If custom source, use the custom name
|
||||
if (sourceName === 'custom') {
|
||||
sourceName = this.form.get('sourceName_custom')?.value;
|
||||
|
||||
// Validate custom source name
|
||||
if (!sourceName || sourceName.trim() === '') {
|
||||
this.form.get('sourceName_custom')?.setErrors({ required: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result: AddMappingResult = {
|
||||
sourceName,
|
||||
sourceData: {
|
||||
id: this.form.get('sourceId')?.value,
|
||||
name: this.form.get('name')?.value || undefined,
|
||||
description: this.form.get('description')?.value || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
122
apps/dashboard/src/app/pages/exchanges/exchanges.component.css
Normal file
122
apps/dashboard/src/app/pages/exchanges/exchanges.component.css
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
.exchanges-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.exchanges-card {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.exchanges-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.exchange-id {
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.source-mappings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.no-mappings {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.source-id {
|
||||
color: #6b7280;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
color: #d1d5db;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.exchanges-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.exchanges-table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<!-- Exchanges Page -->
|
||||
<div class="exchanges-container">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Exchange Management</h1>
|
||||
<p class="page-description">
|
||||
Manage master exchanges and their source mappings across different data providers.
|
||||
</p>
|
||||
<div class="page-actions">
|
||||
<button mat-raised-button color="primary" (click)="syncExchanges()">
|
||||
<mat-icon>sync</mat-icon>
|
||||
Sync Exchanges
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-progress-spinner></mat-progress-spinner>
|
||||
<p>Loading exchanges...</p>
|
||||
</div>
|
||||
|
||||
<!-- Exchanges Table -->
|
||||
<mat-card *ngIf="!loading" class="exchanges-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Master Exchanges ({{ exchanges.length }})</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<table mat-table [dataSource]="exchanges" class="exchanges-table">
|
||||
<!-- Exchange ID Column -->
|
||||
<ng-container matColumnDef="masterExchangeId">
|
||||
<th mat-header-cell *matHeaderCellDef>Exchange ID</th>
|
||||
<td mat-cell *matCellDef="let exchange">
|
||||
<span class="exchange-id">{{ exchange.masterExchangeId }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Source Mappings Column -->
|
||||
<ng-container matColumnDef="sourceMappings">
|
||||
<th mat-header-cell *matHeaderCellDef>Source Mappings</th>
|
||||
<td mat-cell *matCellDef="let exchange">
|
||||
<div class="source-mappings">
|
||||
@if (getSourceMappingKeys(exchange.sourceMappings).length === 0) {
|
||||
<span class="no-mappings">No mappings</span>
|
||||
} @else {
|
||||
<mat-chip-set>
|
||||
@for (source of getSourceMappingKeys(exchange.sourceMappings); track source) {
|
||||
<mat-chip-option>
|
||||
<span class="source-name">{{ source }}</span>
|
||||
<span class="source-id">{{ exchange.sourceMappings[source]?.id }}</span>
|
||||
</mat-chip-option>
|
||||
}
|
||||
</mat-chip-set>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let exchange">
|
||||
<button mat-button color="primary" (click)="openAddMappingDialog(exchange)">
|
||||
<mat-icon>add</mat-icon>
|
||||
Add Mapping
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
|
||||
@if (exchanges.length === 0 && !loading) {
|
||||
<div class="empty-state">
|
||||
<mat-icon class="empty-icon">account_balance</mat-icon>
|
||||
<h3>No exchanges found</h3>
|
||||
<p>Start by syncing exchanges from your data providers.</p>
|
||||
<button mat-raised-button color="primary" (click)="syncExchanges()">
|
||||
<mat-icon>sync</mat-icon>
|
||||
Sync Exchanges
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
110
apps/dashboard/src/app/pages/exchanges/exchanges.component.ts
Normal file
110
apps/dashboard/src/app/pages/exchanges/exchanges.component.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ExchangeService } from '../../services/exchange.service';
|
||||
import { AddMappingDialogComponent } from './dialogs/add-mapping-dialog.component';
|
||||
|
||||
export interface SourceMapping {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface MasterExchange {
|
||||
masterExchangeId: string;
|
||||
sourceMappings: Record<string, SourceMapping>;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-exchanges',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
MatChipsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
],
|
||||
templateUrl: './exchanges.component.html',
|
||||
styleUrl: './exchanges.component.css',
|
||||
})
|
||||
export class ExchangesComponent implements OnInit {
|
||||
exchanges: MasterExchange[] = [];
|
||||
loading = false;
|
||||
displayedColumns: string[] = ['masterExchangeId', 'sourceMappings', 'actions'];
|
||||
|
||||
constructor(
|
||||
private exchangeService: ExchangeService,
|
||||
private dialog: MatDialog,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadExchanges();
|
||||
}
|
||||
|
||||
async loadExchanges() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await this.exchangeService.getAllExchanges();
|
||||
this.exchanges = response.data;
|
||||
} catch {
|
||||
this.snackBar.open('Error loading exchanges', 'Close', { duration: 3000 });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getSourceMappingKeys(sourceMappings: Record<string, SourceMapping>): string[] {
|
||||
return Object.keys(sourceMappings || {});
|
||||
}
|
||||
|
||||
openAddMappingDialog(exchange: MasterExchange) {
|
||||
const dialogRef = this.dialog.open(AddMappingDialogComponent, {
|
||||
width: '500px',
|
||||
data: {
|
||||
masterExchangeId: exchange.masterExchangeId,
|
||||
existingSources: this.getSourceMappingKeys(exchange.sourceMappings),
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.addSourceMapping(exchange.masterExchangeId, result.sourceName, result.sourceData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async addSourceMapping(masterExchangeId: string, sourceName: string, sourceData: SourceMapping) {
|
||||
try {
|
||||
await this.exchangeService.addSourceMapping(masterExchangeId, sourceName, sourceData);
|
||||
this.snackBar.open('Source mapping added successfully', 'Close', { duration: 3000 });
|
||||
this.loadExchanges(); // Refresh the list
|
||||
} catch {
|
||||
this.snackBar.open('Error adding source mapping', 'Close', { duration: 3000 });
|
||||
}
|
||||
}
|
||||
|
||||
async syncExchanges() {
|
||||
try {
|
||||
await this.exchangeService.syncExchanges();
|
||||
this.snackBar.open('Exchange sync initiated', 'Close', { duration: 3000 });
|
||||
} catch {
|
||||
this.snackBar.open('Error syncing exchanges', 'Close', { duration: 3000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
/* Portfolio specific styles */
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
<div class="space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Portfolio</h1>
|
||||
<p class="text-gray-600 mt-1">Manage and monitor your investment portfolio</p>
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
@if (isLoading()) {
|
||||
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
|
||||
}
|
||||
Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Portfolio Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Total Value</p>
|
||||
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().totalValue.toLocaleString() }}</p>
|
||||
</div>
|
||||
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Total P&L</p>
|
||||
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().totalPnL)">
|
||||
{{ portfolioSummary().totalPnL > 0 ? '+' : '' }}${{ portfolioSummary().totalPnL.toLocaleString() }}
|
||||
({{ portfolioSummary().totalPnLPercent.toFixed(2) }}%)
|
||||
</p>
|
||||
</div>
|
||||
<mat-icon class="text-green-600 text-3xl" *ngIf="portfolioSummary().totalPnL >= 0">trending_up</mat-icon>
|
||||
<mat-icon class="text-red-600 text-3xl" *ngIf="portfolioSummary().totalPnL < 0">trending_down</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Day Change</p>
|
||||
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().dayChange)">
|
||||
{{ portfolioSummary().dayChange > 0 ? '+' : '' }}${{ portfolioSummary().dayChange.toLocaleString() }}
|
||||
({{ portfolioSummary().dayChangePercent.toFixed(2) }}%)
|
||||
</p>
|
||||
</div>
|
||||
<mat-icon class="text-purple-600 text-3xl">today</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Cash Available</p>
|
||||
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().cash.toLocaleString() }}</p>
|
||||
</div>
|
||||
<mat-icon class="text-yellow-600 text-3xl">attach_money</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Portfolio Tabs -->
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Positions">
|
||||
<div class="p-6">
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Current Positions</h3>
|
||||
<div class="flex gap-2">
|
||||
<button mat-button>
|
||||
<mat-icon>add</mat-icon>
|
||||
Add Position
|
||||
</button>
|
||||
<button mat-button>
|
||||
<mat-icon>file_download</mat-icon>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span class="ml-3 text-gray-600">Loading portfolio...</span>
|
||||
</div>
|
||||
} @else if (error()) {
|
||||
<div class="text-center py-8">
|
||||
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
|
||||
<p class="text-red-600 mt-2">{{ error() }}</p>
|
||||
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
} @else if (positions().length === 0) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<mat-icon class="text-4xl">account_balance_wallet</mat-icon>
|
||||
<p class="mt-2">No positions found</p>
|
||||
<button mat-button color="primary" class="mt-2">
|
||||
<mat-icon>add</mat-icon>
|
||||
Add Your First Position
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="overflow-x-auto">
|
||||
<table mat-table [dataSource]="positions()" class="mat-elevation-z0 w-full">
|
||||
<!-- Symbol Column -->
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
|
||||
<td mat-cell *matCellDef="let position" class="font-semibold text-gray-900">{{ position.symbol }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Quantity Column -->
|
||||
<ng-container matColumnDef="quantity">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Quantity</th>
|
||||
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
|
||||
{{ position.quantity.toLocaleString() }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Average Price Column -->
|
||||
<ng-container matColumnDef="avgPrice">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Avg Price</th>
|
||||
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
|
||||
${{ position.avgPrice.toFixed(2) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Current Price Column -->
|
||||
<ng-container matColumnDef="currentPrice">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Current Price</th>
|
||||
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
|
||||
${{ position.currentPrice.toFixed(2) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Market Value Column -->
|
||||
<ng-container matColumnDef="marketValue">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Value</th>
|
||||
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
|
||||
${{ position.marketValue.toLocaleString() }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Unrealized P&L Column -->
|
||||
<ng-container matColumnDef="unrealizedPnL">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Unrealized P&L</th>
|
||||
<td mat-cell *matCellDef="let position"
|
||||
class="text-right font-medium"
|
||||
[class]="getPnLColor(position.unrealizedPnL)">
|
||||
{{ position.unrealizedPnL > 0 ? '+' : '' }}${{ position.unrealizedPnL.toLocaleString() }}
|
||||
({{ position.unrealizedPnLPercent.toFixed(2) }}%)
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Day Change Column -->
|
||||
<ng-container matColumnDef="dayChange">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Day Change</th>
|
||||
<td mat-cell *matCellDef="let position"
|
||||
class="text-right font-medium"
|
||||
[class]="getPnLColor(position.dayChange)">
|
||||
{{ position.dayChange > 0 ? '+' : '' }}${{ position.dayChange.toFixed(2) }}
|
||||
({{ position.dayChangePercent.toFixed(2) }}%)
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Performance">
|
||||
<div class="p-6">
|
||||
<mat-card class="p-6 h-96 flex items-center">
|
||||
<div class="text-center text-gray-500 w-full">
|
||||
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">trending_up</mat-icon>
|
||||
<p class="mb-4">Performance charts and analytics will be implemented here</p>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="Orders">
|
||||
<div class="p-6">
|
||||
<mat-card class="p-6 h-96 flex items-center">
|
||||
<div class="text-center text-gray-500 w-full">
|
||||
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt</mat-icon>
|
||||
<p class="mb-4">Order history and management will be implemented here</p>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
avgPrice: number;
|
||||
currentPrice: number;
|
||||
marketValue: number;
|
||||
unrealizedPnL: number;
|
||||
unrealizedPnLPercent: number;
|
||||
dayChange: number;
|
||||
dayChangePercent: number;
|
||||
}
|
||||
|
||||
export interface PortfolioSummary {
|
||||
totalValue: number;
|
||||
totalCost: number;
|
||||
totalPnL: number;
|
||||
totalPnLPercent: number;
|
||||
dayChange: number;
|
||||
dayChangePercent: number;
|
||||
cash: number;
|
||||
positionsCount: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-portfolio',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
MatTabsModule,
|
||||
],
|
||||
templateUrl: './portfolio.component.html',
|
||||
styleUrl: './portfolio.component.css',
|
||||
})
|
||||
export class PortfolioComponent implements OnInit, OnDestroy {
|
||||
private apiService = inject(ApiService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
protected portfolioSummary = signal<PortfolioSummary>({
|
||||
totalValue: 0,
|
||||
totalCost: 0,
|
||||
totalPnL: 0,
|
||||
totalPnLPercent: 0,
|
||||
dayChange: 0,
|
||||
dayChangePercent: 0,
|
||||
cash: 0,
|
||||
positionsCount: 0,
|
||||
});
|
||||
|
||||
protected positions = signal<Position[]>([]);
|
||||
protected isLoading = signal<boolean>(true);
|
||||
protected error = signal<string | null>(null);
|
||||
protected displayedColumns = [
|
||||
'symbol',
|
||||
'quantity',
|
||||
'avgPrice',
|
||||
'currentPrice',
|
||||
'marketValue',
|
||||
'unrealizedPnL',
|
||||
'dayChange',
|
||||
];
|
||||
|
||||
ngOnInit() {
|
||||
this.loadPortfolioData();
|
||||
|
||||
// Refresh portfolio data every 30 seconds
|
||||
const portfolioSubscription = interval(30000).subscribe(() => {
|
||||
this.loadPortfolioData();
|
||||
});
|
||||
this.subscriptions.push(portfolioSubscription);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
private loadPortfolioData() {
|
||||
// Since we don't have a portfolio endpoint yet, let's create mock data
|
||||
// In a real implementation, this would call this.apiService.getPortfolio()
|
||||
|
||||
setTimeout(() => {
|
||||
const mockPositions: Position[] = [
|
||||
{
|
||||
symbol: 'AAPL',
|
||||
quantity: 100,
|
||||
avgPrice: 180.5,
|
||||
currentPrice: 192.53,
|
||||
marketValue: 19253,
|
||||
unrealizedPnL: 1203,
|
||||
unrealizedPnLPercent: 6.67,
|
||||
dayChange: 241,
|
||||
dayChangePercent: 1.27,
|
||||
},
|
||||
{
|
||||
symbol: 'MSFT',
|
||||
quantity: 50,
|
||||
avgPrice: 400.0,
|
||||
currentPrice: 415.26,
|
||||
marketValue: 20763,
|
||||
unrealizedPnL: 763,
|
||||
unrealizedPnLPercent: 3.82,
|
||||
dayChange: 436.5,
|
||||
dayChangePercent: 2.15,
|
||||
},
|
||||
{
|
||||
symbol: 'GOOGL',
|
||||
quantity: 10,
|
||||
avgPrice: 2900.0,
|
||||
currentPrice: 2847.56,
|
||||
marketValue: 28475.6,
|
||||
unrealizedPnL: -524.4,
|
||||
unrealizedPnLPercent: -1.81,
|
||||
dayChange: -123.4,
|
||||
dayChangePercent: -0.43,
|
||||
},
|
||||
];
|
||||
|
||||
const summary: PortfolioSummary = {
|
||||
totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash
|
||||
totalCost: mockPositions.reduce((sum, pos) => sum + pos.avgPrice * pos.quantity, 0),
|
||||
totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0),
|
||||
totalPnLPercent: 0,
|
||||
dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0),
|
||||
dayChangePercent: 0,
|
||||
cash: 25000,
|
||||
positionsCount: mockPositions.length,
|
||||
};
|
||||
|
||||
summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100;
|
||||
summary.dayChangePercent =
|
||||
(summary.dayChange / (summary.totalValue - summary.dayChange)) * 100;
|
||||
|
||||
this.positions.set(mockPositions);
|
||||
this.portfolioSummary.set(summary);
|
||||
this.isLoading.set(false);
|
||||
this.error.set(null);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.isLoading.set(true);
|
||||
this.loadPortfolioData();
|
||||
}
|
||||
|
||||
getPnLColor(value: number): string {
|
||||
if (value > 0) {
|
||||
return 'text-green-600';
|
||||
}
|
||||
if (value < 0) {
|
||||
return 'text-red-600';
|
||||
}
|
||||
return 'text-gray-600';
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
/* Risk Management specific styles */
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
<div class="space-y-6">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Risk Management</h1>
|
||||
<p class="text-gray-600 mt-1">Monitor and control trading risks and exposure</p>
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
@if (isLoading()) {
|
||||
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
|
||||
}
|
||||
Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Risk Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
@if (riskThresholds(); as thresholds) {
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Max Position Size</p>
|
||||
<p class="text-lg font-semibold text-gray-900">${{ thresholds.maxPositionSize.toLocaleString() }}</p>
|
||||
</div>
|
||||
<mat-icon class="text-blue-600 text-3xl">account_balance</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Max Daily Loss</p>
|
||||
<p class="text-lg font-semibold text-red-600">${{ thresholds.maxDailyLoss.toLocaleString() }}</p>
|
||||
</div>
|
||||
<mat-icon class="text-red-600 text-3xl">trending_down</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Portfolio Risk Limit</p>
|
||||
<p class="text-lg font-semibold text-yellow-600">{{ (thresholds.maxPortfolioRisk * 100).toFixed(1) }}%</p>
|
||||
</div>
|
||||
<mat-icon class="text-yellow-600 text-3xl">pie_chart</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Volatility Limit</p>
|
||||
<p class="text-lg font-semibold text-purple-600">{{ (thresholds.volatilityLimit * 100).toFixed(1) }}%</p>
|
||||
</div>
|
||||
<mat-icon class="text-purple-600 text-3xl">show_chart</mat-icon>
|
||||
</div>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Risk Thresholds Configuration -->
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Risk Thresholds Configuration</h3>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span class="ml-3 text-gray-600">Loading risk settings...</span>
|
||||
</div>
|
||||
} @else if (error()) {
|
||||
<div class="text-center py-8">
|
||||
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
|
||||
<p class="text-red-600 mt-2">{{ error() }}</p>
|
||||
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<form [formGroup]="thresholdsForm" (ngSubmit)="saveThresholds()">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Max Position Size ($)</mat-label>
|
||||
<input matInput type="number" formControlName="maxPositionSize" placeholder="100000">
|
||||
<mat-icon matSuffix>attach_money</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Max Daily Loss ($)</mat-label>
|
||||
<input matInput type="number" formControlName="maxDailyLoss" placeholder="5000">
|
||||
<mat-icon matSuffix>trending_down</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Max Portfolio Risk (0-1)</mat-label>
|
||||
<input matInput type="number" step="0.01" formControlName="maxPortfolioRisk" placeholder="0.1">
|
||||
<mat-icon matSuffix>pie_chart</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Volatility Limit (0-1)</mat-label>
|
||||
<input matInput type="number" step="0.01" formControlName="volatilityLimit" placeholder="0.3">
|
||||
<mat-icon matSuffix>show_chart</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="thresholdsForm.invalid || isSaving()">
|
||||
@if (isSaving()) {
|
||||
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
|
||||
}
|
||||
Save Thresholds
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</mat-card>
|
||||
|
||||
<!-- Risk History Table -->
|
||||
<mat-card class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Recent Risk Evaluations</h3>
|
||||
</div>
|
||||
|
||||
@if (riskHistory().length === 0) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<mat-icon class="text-4xl">history</mat-icon>
|
||||
<p class="mt-2">No risk evaluations found</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="overflow-x-auto">
|
||||
<table mat-table [dataSource]="riskHistory()" class="mat-elevation-z0 w-full">
|
||||
<!-- Symbol Column -->
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
|
||||
<td mat-cell *matCellDef="let risk" class="font-semibold text-gray-900">{{ risk.symbol }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Position Value Column -->
|
||||
<ng-container matColumnDef="positionValue">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Position Value</th>
|
||||
<td mat-cell *matCellDef="let risk" class="text-right font-medium text-gray-900">
|
||||
${{ risk.positionValue.toLocaleString() }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Risk Level Column -->
|
||||
<ng-container matColumnDef="riskLevel">
|
||||
<th mat-header-cell *matHeaderCellDef class="text-center font-medium text-gray-900">Risk Level</th>
|
||||
<td mat-cell *matCellDef="let risk" class="text-center">
|
||||
<span class="px-2 py-1 rounded-full text-sm font-medium" [class]="getRiskLevelColor(risk.riskLevel)">
|
||||
{{ risk.riskLevel }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Violations Column -->
|
||||
<ng-container matColumnDef="violations">
|
||||
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Violations</th>
|
||||
<td mat-cell *matCellDef="let risk" class="text-gray-600">
|
||||
@if (risk.violations.length > 0) {
|
||||
<span class="text-red-600">{{ risk.violations.join(', ') }}</span>
|
||||
} @else {
|
||||
<span class="text-green-600">None</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
import { ApiService, RiskEvaluation, RiskThresholds } from '../../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-risk-management',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSnackBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './risk-management.component.html',
|
||||
styleUrl: './risk-management.component.css',
|
||||
})
|
||||
export class RiskManagementComponent implements OnInit, OnDestroy {
|
||||
private apiService = inject(ApiService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private fb = inject(FormBuilder);
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
protected riskThresholds = signal<RiskThresholds | null>(null);
|
||||
protected riskHistory = signal<RiskEvaluation[]>([]);
|
||||
protected isLoading = signal<boolean>(true);
|
||||
protected isSaving = signal<boolean>(false);
|
||||
protected error = signal<string | null>(null);
|
||||
|
||||
protected thresholdsForm: FormGroup;
|
||||
protected displayedColumns = ['symbol', 'positionValue', 'riskLevel', 'violations', 'timestamp'];
|
||||
|
||||
constructor() {
|
||||
this.thresholdsForm = this.fb.group({
|
||||
maxPositionSize: [0, [Validators.required, Validators.min(0)]],
|
||||
maxDailyLoss: [0, [Validators.required, Validators.min(0)]],
|
||||
maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
|
||||
volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadRiskThresholds();
|
||||
this.loadRiskHistory();
|
||||
|
||||
// Refresh risk history every 30 seconds
|
||||
const historySubscription = interval(30000).subscribe(() => {
|
||||
this.loadRiskHistory();
|
||||
});
|
||||
this.subscriptions.push(historySubscription);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
private loadRiskThresholds() {
|
||||
this.apiService.getRiskThresholds().subscribe({
|
||||
next: response => {
|
||||
this.riskThresholds.set(response.data);
|
||||
this.thresholdsForm.patchValue(response.data);
|
||||
this.isLoading.set(false);
|
||||
this.error.set(null);
|
||||
},
|
||||
error: err => {
|
||||
console.error('Failed to load risk thresholds:', err);
|
||||
this.error.set('Failed to load risk thresholds');
|
||||
this.isLoading.set(false);
|
||||
this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private loadRiskHistory() {
|
||||
this.apiService.getRiskHistory().subscribe({
|
||||
next: response => {
|
||||
this.riskHistory.set(response.data);
|
||||
},
|
||||
error: err => {
|
||||
console.error('Failed to load risk history:', err);
|
||||
this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
saveThresholds() {
|
||||
if (this.thresholdsForm.valid) {
|
||||
this.isSaving.set(true);
|
||||
const thresholds = this.thresholdsForm.value as RiskThresholds;
|
||||
|
||||
this.apiService.updateRiskThresholds(thresholds).subscribe({
|
||||
next: response => {
|
||||
this.riskThresholds.set(response.data);
|
||||
this.isSaving.set(false);
|
||||
this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 });
|
||||
},
|
||||
error: err => {
|
||||
console.error('Failed to save risk thresholds:', err);
|
||||
this.isSaving.set(false);
|
||||
this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.isLoading.set(true);
|
||||
this.loadRiskThresholds();
|
||||
this.loadRiskHistory();
|
||||
}
|
||||
|
||||
getRiskLevelColor(level: string): string {
|
||||
switch (level) {
|
||||
case 'LOW':
|
||||
return 'text-green-600';
|
||||
case 'MEDIUM':
|
||||
return 'text-yellow-600';
|
||||
case 'HIGH':
|
||||
return 'text-red-600';
|
||||
default:
|
||||
return 'text-gray-600';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Chart, ChartOptions } from 'chart.js/auto';
|
||||
import { BacktestResult } from '../../../services/strategy.service';
|
||||
|
||||
@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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Chart, ChartOptions } from 'chart.js/auto';
|
||||
import { BacktestResult } from '../../../services/strategy.service';
|
||||
|
||||
@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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import {
|
||||
BacktestRequest,
|
||||
BacktestResult,
|
||||
StrategyService,
|
||||
TradingStrategy,
|
||||
} from '../../../services/strategy.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-backtest-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatProgressBarModule,
|
||||
MatTabsModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
MatSlideToggleModule,
|
||||
],
|
||||
templateUrl: './backtest-dialog.component.html',
|
||||
styleUrl: './backtest-dialog.component.css',
|
||||
})
|
||||
export class BacktestDialogComponent implements OnInit {
|
||||
backtestForm: FormGroup;
|
||||
strategyTypes: string[] = [];
|
||||
availableSymbols: string[] = [
|
||||
'AAPL',
|
||||
'MSFT',
|
||||
'GOOGL',
|
||||
'AMZN',
|
||||
'TSLA',
|
||||
'META',
|
||||
'NVDA',
|
||||
'SPY',
|
||||
'QQQ',
|
||||
];
|
||||
selectedSymbols: string[] = [];
|
||||
parameters: Record<string, any> = {};
|
||||
isRunning: boolean = false;
|
||||
backtestResult: BacktestResult | null = null;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private strategyService: StrategyService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
|
||||
private dialogRef: MatDialogRef<BacktestDialogComponent>
|
||||
) {
|
||||
// Initialize form with defaults
|
||||
this.backtestForm = this.fb.group({
|
||||
strategyType: ['', [Validators.required]],
|
||||
startDate: [
|
||||
new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
|
||||
[Validators.required],
|
||||
],
|
||||
endDate: [new Date(), [Validators.required]],
|
||||
initialCapital: [100000, [Validators.required, Validators.min(1000)]],
|
||||
dataResolution: ['1d', [Validators.required]],
|
||||
commission: [0.001, [Validators.required, Validators.min(0), Validators.max(0.1)]],
|
||||
slippage: [0.0005, [Validators.required, Validators.min(0), Validators.max(0.1)]],
|
||||
mode: ['event', [Validators.required]],
|
||||
});
|
||||
|
||||
// If strategy is provided, pre-populate the form
|
||||
if (data) {
|
||||
this.selectedSymbols = [...data.symbols];
|
||||
this.backtestForm.patchValue({
|
||||
strategyType: data.type,
|
||||
});
|
||||
this.parameters = { ...data.parameters };
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadStrategyTypes();
|
||||
}
|
||||
|
||||
loadStrategyTypes(): void {
|
||||
this.strategyService.getStrategyTypes().subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
this.strategyTypes = response.data;
|
||||
|
||||
// If strategy is provided, load its parameters
|
||||
if (this.data) {
|
||||
this.onStrategyTypeChange(this.data.type);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error loading strategy types:', error);
|
||||
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStrategyTypeChange(type: string): void {
|
||||
// Get default parameters for this strategy type
|
||||
this.strategyService.getStrategyParameters(type).subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
// If strategy is provided, merge default with existing
|
||||
if (this.data) {
|
||||
this.parameters = {
|
||||
...response.data,
|
||||
...this.data.parameters,
|
||||
};
|
||||
} else {
|
||||
this.parameters = response.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error loading parameters:', error);
|
||||
this.parameters = {};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addSymbol(symbol: string): void {
|
||||
if (!symbol || this.selectedSymbols.includes(symbol)) {
|
||||
return;
|
||||
}
|
||||
this.selectedSymbols.push(symbol);
|
||||
}
|
||||
|
||||
removeSymbol(symbol: string): void {
|
||||
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
|
||||
}
|
||||
|
||||
updateParameter(key: string, value: any): void {
|
||||
this.parameters[key] = value;
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.backtestForm.invalid || this.selectedSymbols.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.backtestForm.value;
|
||||
|
||||
const backtestRequest: BacktestRequest = {
|
||||
strategyType: formValue.strategyType,
|
||||
strategyParams: this.parameters,
|
||||
symbols: this.selectedSymbols,
|
||||
startDate: formValue.startDate,
|
||||
endDate: formValue.endDate,
|
||||
initialCapital: formValue.initialCapital,
|
||||
dataResolution: formValue.dataResolution,
|
||||
commission: formValue.commission,
|
||||
slippage: formValue.slippage,
|
||||
mode: formValue.mode,
|
||||
};
|
||||
|
||||
this.isRunning = true;
|
||||
|
||||
this.strategyService.runBacktest(backtestRequest).subscribe({
|
||||
next: response => {
|
||||
this.isRunning = false;
|
||||
if (response.success) {
|
||||
this.backtestResult = response.data;
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
this.isRunning = false;
|
||||
console.error('Backtest error:', error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close(this.backtestResult);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<h2 mat-dialog-title>{{isEditMode ? 'Edit Strategy' : 'Create Strategy'}}</h2>
|
||||
|
||||
<form [formGroup]="strategyForm" (ngSubmit)="onSubmit()">
|
||||
<mat-dialog-content class="mat-typography">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<!-- Basic Strategy Information -->
|
||||
<mat-form-field appearance="outline" class="w-full">
|
||||
<mat-label>Strategy Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="e.g., My Moving Average Crossover">
|
||||
<mat-error *ngIf="strategyForm.get('name')?.invalid">Name is required</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="w-full">
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea matInput formControlName="description" rows="3"
|
||||
placeholder="Describe what this strategy does..."></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="w-full">
|
||||
<mat-label>Strategy Type</mat-label>
|
||||
<mat-select formControlName="type" (selectionChange)="onStrategyTypeChange($event.value)">
|
||||
<mat-option *ngFor="let type of strategyTypes" [value]="type">
|
||||
{{type}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="strategyForm.get('type')?.invalid">Strategy type is required</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Symbol Selection -->
|
||||
<div class="w-full">
|
||||
<label class="text-sm">Trading Symbols</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<mat-chip *ngFor="let symbol of selectedSymbols" [removable]="true"
|
||||
(removed)="removeSymbol(symbol)">
|
||||
{{symbol}}
|
||||
<mat-icon matChipRemove>cancel</mat-icon>
|
||||
</mat-chip>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<mat-form-field appearance="outline" class="flex-1">
|
||||
<mat-label>Add Symbol</mat-label>
|
||||
<input matInput #symbolInput placeholder="e.g., AAPL">
|
||||
</mat-form-field>
|
||||
<button type="button" mat-raised-button color="primary"
|
||||
(click)="addSymbol(symbolInput.value); symbolInput.value = ''">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500 mb-1">Suggested symbols:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button" *ngFor="let symbol of availableSymbols"
|
||||
mat-stroked-button (click)="addSymbol(symbol)"
|
||||
[disabled]="selectedSymbols.includes(symbol)">
|
||||
{{symbol}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Strategy Parameters -->
|
||||
<div *ngIf="strategyForm.get('type')?.value && Object.keys(parameters).length > 0">
|
||||
<h3 class="text-lg font-semibold mb-2">Strategy Parameters</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<mat-form-field *ngFor="let param of parameters | keyvalue" appearance="outline">
|
||||
<mat-label>{{param.key}}</mat-label>
|
||||
<input matInput [value]="param.value"
|
||||
(input)="updateParameter(param.key, $any($event.target).value)">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-raised-button color="primary" type="submit"
|
||||
[disabled]="strategyForm.invalid || selectedSymbols.length === 0">
|
||||
{{isEditMode ? 'Update' : 'Create'}}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { StrategyService, TradingStrategy } from '../../../services/strategy.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-strategy-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
MatAutocompleteModule,
|
||||
],
|
||||
templateUrl: './strategy-dialog.component.html',
|
||||
styleUrl: './strategy-dialog.component.css',
|
||||
})
|
||||
export class StrategyDialogComponent implements OnInit {
|
||||
strategyForm: FormGroup;
|
||||
isEditMode: boolean = false;
|
||||
strategyTypes: string[] = [];
|
||||
availableSymbols: string[] = [
|
||||
'AAPL',
|
||||
'MSFT',
|
||||
'GOOGL',
|
||||
'AMZN',
|
||||
'TSLA',
|
||||
'META',
|
||||
'NVDA',
|
||||
'SPY',
|
||||
'QQQ',
|
||||
];
|
||||
selectedSymbols: string[] = [];
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
parameters: Record<string, any> = {};
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private strategyService: StrategyService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
|
||||
private dialogRef: MatDialogRef<StrategyDialogComponent>
|
||||
) {
|
||||
this.isEditMode = !!data;
|
||||
|
||||
this.strategyForm = this.fb.group({
|
||||
name: ['', [Validators.required]],
|
||||
description: [''],
|
||||
type: ['', [Validators.required]],
|
||||
// Dynamic parameters will be added based on strategy type
|
||||
});
|
||||
|
||||
if (this.isEditMode && data) {
|
||||
this.selectedSymbols = [...data.symbols];
|
||||
this.strategyForm.patchValue({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
type: data.type,
|
||||
});
|
||||
this.parameters = { ...data.parameters };
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// In a real implementation, fetch available strategy types from the API
|
||||
this.loadStrategyTypes();
|
||||
}
|
||||
|
||||
loadStrategyTypes(): void {
|
||||
// In a real implementation, this would call the API
|
||||
this.strategyService.getStrategyTypes().subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
this.strategyTypes = response.data;
|
||||
|
||||
// If editing, load parameters
|
||||
if (this.isEditMode && this.data) {
|
||||
this.onStrategyTypeChange(this.data.type);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error loading strategy types:', error);
|
||||
// Fallback to hardcoded types
|
||||
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStrategyTypeChange(type: string): void {
|
||||
// Get default parameters for this strategy type
|
||||
this.strategyService.getStrategyParameters(type).subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
// If editing, merge default with existing
|
||||
if (this.isEditMode && this.data) {
|
||||
this.parameters = {
|
||||
...response.data,
|
||||
...this.data.parameters,
|
||||
};
|
||||
} else {
|
||||
this.parameters = response.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error loading parameters:', error);
|
||||
// Fallback to empty parameters
|
||||
this.parameters = {};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
addSymbol(symbol: string): void {
|
||||
if (!symbol || this.selectedSymbols.includes(symbol)) {
|
||||
return;
|
||||
}
|
||||
this.selectedSymbols.push(symbol);
|
||||
}
|
||||
|
||||
removeSymbol(symbol: string): void {
|
||||
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.strategyForm.invalid || this.selectedSymbols.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.strategyForm.value;
|
||||
|
||||
const strategy: Partial<TradingStrategy> = {
|
||||
name: formValue.name,
|
||||
description: formValue.description,
|
||||
type: formValue.type,
|
||||
symbols: this.selectedSymbols,
|
||||
parameters: this.parameters,
|
||||
};
|
||||
|
||||
if (this.isEditMode && this.data) {
|
||||
this.strategyService.updateStrategy(this.data.id, strategy).subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error updating strategy:', error);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.strategyService.createStrategy(strategy).subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error creating strategy:', error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateParameter(key: string, value: any): void {
|
||||
this.parameters[key] = value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
/* Strategies specific styles */
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Trading Strategies</h1>
|
||||
<p class="text-gray-600 mt-1">Configure and monitor your automated trading strategies</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button mat-raised-button color="primary" (click)="openStrategyDialog()">
|
||||
<mat-icon>add</mat-icon> New Strategy
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="openBacktestDialog()">
|
||||
<mat-icon>science</mat-icon> New Backtest
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-card *ngIf="isLoading" class="p-4">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</mat-card>
|
||||
|
||||
<div *ngIf="!selectedStrategy; else strategyDetails">
|
||||
<mat-card *ngIf="strategies.length > 0; else noStrategies" class="p-4">
|
||||
<table mat-table [dataSource]="strategies" class="w-full">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Strategy</th>
|
||||
<td mat-cell *matCellDef="let strategy">
|
||||
<div class="font-semibold">{{strategy.name}}</div>
|
||||
<div class="text-xs text-gray-500">{{strategy.description}}</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Type Column -->
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>Type</th>
|
||||
<td mat-cell *matCellDef="let strategy">{{strategy.type}}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Symbols Column -->
|
||||
<ng-container matColumnDef="symbols">
|
||||
<th mat-header-cell *matHeaderCellDef>Symbols</th>
|
||||
<td mat-cell *matCellDef="let strategy">
|
||||
<div class="flex flex-wrap gap-1 max-w-xs">
|
||||
<mat-chip *ngFor="let symbol of strategy.symbols.slice(0, 3)">
|
||||
{{symbol}}
|
||||
</mat-chip>
|
||||
<span *ngIf="strategy.symbols.length > 3" class="text-gray-500">
|
||||
+{{strategy.symbols.length - 3}} more
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let strategy">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full"
|
||||
[style.background-color]="getStatusColor(strategy.status)"></span>
|
||||
{{strategy.status}}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Performance Column -->
|
||||
<ng-container matColumnDef="performance">
|
||||
<th mat-header-cell *matHeaderCellDef>Performance</th>
|
||||
<td mat-cell *matCellDef="let strategy">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-xs text-gray-500">Return:</span>
|
||||
<span [ngClass]="{'text-green-600': strategy.performance.totalReturn > 0,
|
||||
'text-red-600': strategy.performance.totalReturn < 0}">
|
||||
{{strategy.performance.totalReturn | percent:'1.2-2'}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-xs text-gray-500">Win Rate:</span>
|
||||
<span>{{strategy.performance.winRate | percent:'1.0-0'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let strategy">
|
||||
<div class="flex gap-2">
|
||||
<button mat-icon-button color="primary" (click)="viewStrategyDetails(strategy)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [color]="strategy.status === 'ACTIVE' ? 'warn' : 'primary'"
|
||||
(click)="toggleStrategyStatus(strategy)">
|
||||
<mat-icon>{{strategy.status === 'ACTIVE' ? 'pause' : 'play_arrow'}}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="openStrategyDialog(strategy)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openBacktestDialog(strategy)">
|
||||
<mat-icon>science</mat-icon>
|
||||
<span>Backtest</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</mat-card>
|
||||
|
||||
<ng-template #noStrategies>
|
||||
<mat-card class="p-6 flex flex-col items-center justify-center">
|
||||
<div class="text-center text-gray-500 w-full">
|
||||
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem; margin: 0 auto;">psychology</mat-icon>
|
||||
<h3 class="text-xl font-semibold mt-4">No Strategies Yet</h3>
|
||||
<p class="mb-4">Create your first trading strategy to get started</p>
|
||||
<button mat-raised-button color="primary" (click)="openStrategyDialog()">
|
||||
<mat-icon>add</mat-icon> Create Strategy
|
||||
</button>
|
||||
</div>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-template #strategyDetails>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<button mat-button (click)="selectedStrategy = null">
|
||||
<mat-icon>arrow_back</mat-icon> Back to Strategies
|
||||
</button>
|
||||
</div>
|
||||
<app-strategy-details [strategy]="selectedStrategy"></app-strategy-details>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { StrategyService, TradingStrategy } from '../../services/strategy.service';
|
||||
import { WebSocketService } from '../../services/websocket.service';
|
||||
import { BacktestDialogComponent } from './dialogs/backtest-dialog.component';
|
||||
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
|
||||
import { StrategyDetailsComponent } from './strategy-details/strategy-details.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-strategies',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTabsModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatPaginatorModule,
|
||||
MatDialogModule,
|
||||
MatMenuModule,
|
||||
MatChipsModule,
|
||||
MatProgressBarModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
StrategyDetailsComponent,
|
||||
],
|
||||
templateUrl: './strategies.component.html',
|
||||
styleUrl: './strategies.component.css',
|
||||
})
|
||||
export class StrategiesComponent implements OnInit {
|
||||
strategies: TradingStrategy[] = [];
|
||||
displayedColumns: string[] = ['name', 'type', 'symbols', 'status', 'performance', 'actions'];
|
||||
selectedStrategy: TradingStrategy | null = null;
|
||||
isLoading = false;
|
||||
|
||||
constructor(
|
||||
private strategyService: StrategyService,
|
||||
private webSocketService: WebSocketService,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadStrategies();
|
||||
this.listenForStrategyUpdates();
|
||||
}
|
||||
|
||||
loadStrategies(): void {
|
||||
this.isLoading = true;
|
||||
this.strategyService.getStrategies().subscribe({
|
||||
next: response => {
|
||||
if (response.success) {
|
||||
this.strategies = response.data;
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error loading strategies:', error);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
listenForStrategyUpdates(): void {
|
||||
this.webSocketService.messages.subscribe(message => {
|
||||
if (
|
||||
message.type === 'STRATEGY_CREATED' ||
|
||||
message.type === 'STRATEGY_UPDATED' ||
|
||||
message.type === 'STRATEGY_STATUS_CHANGED'
|
||||
) {
|
||||
// Refresh the strategy list when changes occur
|
||||
this.loadStrategies();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return 'green';
|
||||
case 'PAUSED':
|
||||
return 'orange';
|
||||
case 'ERROR':
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
openStrategyDialog(strategy?: TradingStrategy): void {
|
||||
const dialogRef = this.dialog.open(StrategyDialogComponent, {
|
||||
width: '600px',
|
||||
data: strategy || null,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadStrategies();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openBacktestDialog(strategy?: TradingStrategy): void {
|
||||
const dialogRef = this.dialog.open(BacktestDialogComponent, {
|
||||
width: '800px',
|
||||
data: strategy || null,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
// Handle backtest result if needed
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleStrategyStatus(strategy: TradingStrategy): void {
|
||||
this.isLoading = true;
|
||||
|
||||
if (strategy.status === 'ACTIVE') {
|
||||
this.strategyService.pauseStrategy(strategy.id).subscribe({
|
||||
next: () => this.loadStrategies(),
|
||||
error: error => {
|
||||
console.error('Error pausing strategy:', error);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.strategyService.startStrategy(strategy.id).subscribe({
|
||||
next: () => this.loadStrategies(),
|
||||
error: error => {
|
||||
console.error('Error starting strategy:', error);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
viewStrategyDetails(strategy: TradingStrategy): void {
|
||||
this.selectedStrategy = strategy;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/* Strategy details specific styles */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
<div class="space-y-6" *ngIf="strategy">
|
||||
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
|
||||
<!-- Strategy Overview Card -->
|
||||
<mat-card class="flex-1 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">{{ strategy.name }}</h2>
|
||||
<p class="text-gray-600 text-sm">{{ strategy.description }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button mat-raised-button color="primary" class="mr-2" (click)="openBacktestDialog()">
|
||||
Run Backtest
|
||||
</button>
|
||||
<span
|
||||
class="px-3 py-1 rounded-full text-xs font-semibold"
|
||||
[style.background-color]="getStatusColor(strategy.status)"
|
||||
style="color: white"
|
||||
>
|
||||
{{ strategy.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm text-gray-600">Type</h3>
|
||||
<p>{{ strategy.type }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm text-gray-600">Created</h3>
|
||||
<p>{{ strategy.createdAt | date: 'medium' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm text-gray-600">Last Updated</h3>
|
||||
<p>{{ strategy.updatedAt | date: 'medium' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm text-gray-600">Symbols</h3>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<mat-chip *ngFor="let symbol of strategy.symbols">{{ symbol }}</mat-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
<!-- Performance Summary Card -->
|
||||
<mat-card class="md:w-1/3 p-4">
|
||||
<h3 class="text-lg font-bold mb-3">Performance</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Return</p>
|
||||
<p
|
||||
class="text-xl font-semibold"
|
||||
[ngClass]="{
|
||||
'text-green-600': performance.totalReturn >= 0,
|
||||
'text-red-600': performance.totalReturn < 0,
|
||||
}"
|
||||
>
|
||||
{{ performance.totalReturn | percent: '1.2-2' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Win Rate</p>
|
||||
<p class="text-xl font-semibold">{{ performance.winRate | percent: '1.0-0' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Sharpe Ratio</p>
|
||||
<p class="text-xl font-semibold">{{ performance.sharpeRatio | number: '1.2-2' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Max Drawdown</p>
|
||||
<p class="text-xl font-semibold text-red-600">
|
||||
{{ performance.maxDrawdown | percent: '1.2-2' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Total Trades</p>
|
||||
<p class="text-xl font-semibold">{{ performance.totalTrades }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Sortino Ratio</p>
|
||||
<p class="text-xl font-semibold">{{ performance.sortinoRatio | number: '1.2-2' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="my-4"></mat-divider>
|
||||
|
||||
<div class="flex justify-between mt-2">
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
*ngIf="strategy.status !== 'ACTIVE'"
|
||||
(click)="activateStrategy()"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon> Start
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
*ngIf="strategy.status === 'ACTIVE'"
|
||||
(click)="pauseStrategy()"
|
||||
>
|
||||
<mat-icon>pause</mat-icon> Pause
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
color="warn"
|
||||
*ngIf="strategy.status === 'ACTIVE'"
|
||||
(click)="stopStrategy()"
|
||||
>
|
||||
<mat-icon>stop</mat-icon> Stop
|
||||
</button>
|
||||
<button mat-button (click)="openEditDialog()"><mat-icon>edit</mat-icon> Edit</button>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Card -->
|
||||
<mat-card class="p-4">
|
||||
<h3 class="text-lg font-bold mb-3">Strategy Parameters</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div *ngFor="let param of strategy.parameters | keyvalue">
|
||||
<p class="text-sm text-gray-600">{{ param.key }}</p>
|
||||
<p class="font-semibold">{{ param.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
<!-- Backtest Results Section (only shown when a backtest has been run) -->
|
||||
<div *ngIf="backtestResult" class="backtest-results space-y-6">
|
||||
<h2 class="text-xl font-bold">Backtest Results</h2>
|
||||
|
||||
<!-- Performance Metrics Component -->
|
||||
<app-performance-metrics [backtestResult]="backtestResult"></app-performance-metrics>
|
||||
|
||||
<!-- Equity Chart Component -->
|
||||
<app-equity-chart [backtestResult]="backtestResult"></app-equity-chart>
|
||||
|
||||
<!-- Drawdown Chart Component -->
|
||||
<app-drawdown-chart [backtestResult]="backtestResult"></app-drawdown-chart>
|
||||
|
||||
<!-- Trades Table Component -->
|
||||
<app-trades-table [backtestResult]="backtestResult"></app-trades-table>
|
||||
</div>
|
||||
|
||||
<!-- Tabs for Signals/Trades -->
|
||||
<mat-card class="p-0">
|
||||
<mat-tab-group>
|
||||
<!-- Signals Tab -->
|
||||
<mat-tab label="Recent Signals">
|
||||
<div class="p-4">
|
||||
<ng-container *ngIf="!isLoadingSignals; else loadingSignals">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 text-left">Time</th>
|
||||
<th class="py-2 text-left">Symbol</th>
|
||||
<th class="py-2 text-left">Action</th>
|
||||
<th class="py-2 text-left">Price</th>
|
||||
<th class="py-2 text-left">Quantity</th>
|
||||
<th class="py-2 text-left">Confidence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let signal of signals">
|
||||
<td class="py-2">{{ signal.timestamp | date: 'short' }}</td>
|
||||
<td class="py-2">{{ signal.symbol }}</td>
|
||||
<td class="py-2">
|
||||
<span
|
||||
class="px-2 py-1 rounded text-xs font-semibold"
|
||||
[style.background-color]="getSignalColor(signal.action)"
|
||||
style="color: white"
|
||||
>
|
||||
{{ signal.action }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2">${{ signal.price | number: '1.2-2' }}</td>
|
||||
<td class="py-2">{{ signal.quantity }}</td>
|
||||
<td class="py-2">{{ signal.confidence | percent: '1.0-0' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-template #loadingSignals>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Trades Tab -->
|
||||
<mat-tab label="Recent Trades">
|
||||
<div class="p-4">
|
||||
<ng-container *ngIf="!isLoadingTrades; else loadingTrades">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 text-left">Symbol</th>
|
||||
<th class="py-2 text-left">Entry</th>
|
||||
<th class="py-2 text-left">Exit</th>
|
||||
<th class="py-2 text-left">Quantity</th>
|
||||
<th class="py-2 text-left">P&L</th>
|
||||
<th class="py-2 text-left">P&L %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let trade of trades">
|
||||
<td class="py-2">{{ trade.symbol }}</td>
|
||||
<td class="py-2">
|
||||
${{ trade.entryPrice | number: '1.2-2' }} @
|
||||
{{ trade.entryTime | date: 'short' }}
|
||||
</td>
|
||||
<td class="py-2">
|
||||
${{ trade.exitPrice | number: '1.2-2' }} @
|
||||
{{ trade.exitTime | date: 'short' }}
|
||||
</td>
|
||||
<td class="py-2">{{ trade.quantity }}</td>
|
||||
<td
|
||||
class="py-2"
|
||||
[ngClass]="{ 'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0 }"
|
||||
>
|
||||
${{ trade.pnl | number: '1.2-2' }}
|
||||
</td>
|
||||
<td
|
||||
class="py-2"
|
||||
[ngClass]="{
|
||||
'text-green-600': trade.pnlPercent >= 0,
|
||||
'text-red-600': trade.pnlPercent < 0,
|
||||
}"
|
||||
>
|
||||
{{ trade.pnlPercent | number: '1.2-2' }}%
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-template #loadingTrades>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<mat-card class="p-6 flex items-center" *ngIf="!strategy">
|
||||
<div class="text-center text-gray-500 w-full">
|
||||
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem">psychology</mat-icon>
|
||||
<p class="mb-4">No strategy selected</p>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
|
@ -1,415 +0,0 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import {
|
||||
BacktestResult,
|
||||
StrategyService,
|
||||
TradingStrategy,
|
||||
} from '../../../services/strategy.service';
|
||||
import { WebSocketService } from '../../../services/websocket.service';
|
||||
import { DrawdownChartComponent } from '../components/drawdown-chart.component';
|
||||
import { EquityChartComponent } from '../components/equity-chart.component';
|
||||
import { PerformanceMetricsComponent } from '../components/performance-metrics.component';
|
||||
import { TradesTableComponent } from '../components/trades-table.component';
|
||||
import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
|
||||
import { StrategyDialogComponent } from '../dialogs/strategy-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 && 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,
|
||||
winningTrades,
|
||||
losingTrades,
|
||||
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;
|
||||
}
|
||||
}
|
||||
89
apps/dashboard/src/app/services/breadcrumb.service.ts
Normal file
89
apps/dashboard/src/app/services/breadcrumb.service.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
url: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BreadcrumbService {
|
||||
private breadcrumbsSubject = new BehaviorSubject<BreadcrumbItem[]>([]);
|
||||
public breadcrumbs$ = this.breadcrumbsSubject.asObservable();
|
||||
|
||||
private routeLabels: Record<string, string> = {
|
||||
'/dashboard': 'Dashboard',
|
||||
'/market-data': 'Market Data',
|
||||
'/exchanges': 'Exchanges',
|
||||
'/settings': 'Settings',
|
||||
};
|
||||
|
||||
constructor(private router: Router) {
|
||||
this.router.events
|
||||
.pipe(filter(event => event instanceof NavigationEnd))
|
||||
.subscribe((event: NavigationEnd) => {
|
||||
this.updateBreadcrumbs(event.url);
|
||||
});
|
||||
}
|
||||
|
||||
private updateBreadcrumbs(url: string) {
|
||||
const pathSegments = url.split('/').filter(segment => segment);
|
||||
const breadcrumbs: BreadcrumbItem[] = [];
|
||||
|
||||
// Always add home/dashboard as first breadcrumb
|
||||
breadcrumbs.push({
|
||||
label: 'Home',
|
||||
url: '/dashboard',
|
||||
isActive: url === '/dashboard' || url === '/',
|
||||
});
|
||||
|
||||
// Build breadcrumbs for current path
|
||||
let currentPath = '';
|
||||
pathSegments.forEach((segment, index) => {
|
||||
currentPath += `/${segment}`;
|
||||
const isLast = index === pathSegments.length - 1;
|
||||
|
||||
// Skip if it's the dashboard route (already added as Home)
|
||||
if (currentPath === '/dashboard') {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = this.getRouteLabel(currentPath, segment);
|
||||
breadcrumbs.push({
|
||||
label,
|
||||
url: currentPath,
|
||||
isActive: isLast,
|
||||
});
|
||||
});
|
||||
|
||||
this.breadcrumbsSubject.next(breadcrumbs);
|
||||
}
|
||||
|
||||
private getRouteLabel(path: string, segment: string): string {
|
||||
// Check if we have a custom label for this path
|
||||
if (this.routeLabels[path]) {
|
||||
return this.routeLabels[path];
|
||||
}
|
||||
|
||||
// Convert kebab-case to Title Case
|
||||
return segment
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
public getCurrentPageTitle(): string {
|
||||
const breadcrumbs = this.breadcrumbsSubject.value;
|
||||
const activeBreadcrumb = breadcrumbs.find(item => item.isActive);
|
||||
return activeBreadcrumb?.label || 'Dashboard';
|
||||
}
|
||||
|
||||
public addRouteLabel(path: string, label: string) {
|
||||
this.routeLabels[path] = label;
|
||||
}
|
||||
}
|
||||
116
apps/dashboard/src/app/services/exchange.service.ts
Normal file
116
apps/dashboard/src/app/services/exchange.service.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
export interface SourceMapping {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ExchangeResponse {
|
||||
data: Array<{
|
||||
masterExchangeId: string;
|
||||
sourceMappings: Record<string, SourceMapping>;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}>;
|
||||
count: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AddMappingResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
masterExchangeId: string;
|
||||
sourceName: string;
|
||||
sourceData: SourceMapping;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SyncResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
jobId: string;
|
||||
operation: string;
|
||||
}
|
||||
|
||||
export interface GetExchangeResponse {
|
||||
status: string;
|
||||
data: {
|
||||
masterExchangeId: string;
|
||||
sourceMappings: Record<string, SourceMapping>;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetExchangesBySourceResponse {
|
||||
data: Array<{
|
||||
masterExchangeId: string;
|
||||
sourceMapping: SourceMapping;
|
||||
}>;
|
||||
count: number;
|
||||
status: string;
|
||||
sourceName: string;
|
||||
}
|
||||
|
||||
export interface GetSourceMappingResponse {
|
||||
status: string;
|
||||
data: {
|
||||
masterExchangeId: string;
|
||||
sourceName: string;
|
||||
sourceId: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ExchangeService {
|
||||
private readonly baseUrl = 'http://localhost:2001/api/exchanges';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
async getAllExchanges(): Promise<ExchangeResponse> {
|
||||
return this.http.get<ExchangeResponse>(this.baseUrl).toPromise() as Promise<ExchangeResponse>;
|
||||
}
|
||||
|
||||
async getExchange(masterExchangeId: string): Promise<GetExchangeResponse> {
|
||||
return this.http
|
||||
.get<GetExchangeResponse>(`${this.baseUrl}/${masterExchangeId}`)
|
||||
.toPromise() as Promise<GetExchangeResponse>;
|
||||
}
|
||||
|
||||
async addSourceMapping(
|
||||
masterExchangeId: string,
|
||||
sourceName: string,
|
||||
sourceData: SourceMapping
|
||||
): Promise<AddMappingResponse> {
|
||||
return this.http
|
||||
.post<AddMappingResponse>(`${this.baseUrl}/${masterExchangeId}/mappings`, {
|
||||
sourceName,
|
||||
sourceData,
|
||||
})
|
||||
.toPromise() as Promise<AddMappingResponse>;
|
||||
}
|
||||
|
||||
async syncExchanges(): Promise<SyncResponse> {
|
||||
return this.http
|
||||
.post<SyncResponse>(`${this.baseUrl}/sync`, {})
|
||||
.toPromise() as Promise<SyncResponse>;
|
||||
}
|
||||
|
||||
async getExchangesBySource(sourceName: string): Promise<GetExchangesBySourceResponse> {
|
||||
return this.http
|
||||
.get<GetExchangesBySourceResponse>(`${this.baseUrl}/source/${sourceName}`)
|
||||
.toPromise() as Promise<GetExchangesBySourceResponse>;
|
||||
}
|
||||
|
||||
async getSourceMapping(sourceName: string, sourceId: string): Promise<GetSourceMappingResponse> {
|
||||
return this.http
|
||||
.get<GetSourceMappingResponse>(`${this.baseUrl}/mapping/${sourceName}/${sourceId}`)
|
||||
.toPromise() as Promise<GetSourceMappingResponse>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface TradingStrategy {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'PAUSED' | 'ERROR';
|
||||
type: string;
|
||||
symbols: string[];
|
||||
parameters: Record<string, any>;
|
||||
performance: {
|
||||
totalTrades: number;
|
||||
winRate: number;
|
||||
totalReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
};
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface BacktestRequest {
|
||||
strategyType: string;
|
||||
strategyParams: Record<string, any>;
|
||||
symbols: string[];
|
||||
startDate: Date | string;
|
||||
endDate: Date | string;
|
||||
initialCapital: number;
|
||||
dataResolution: '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
|
||||
commission: number;
|
||||
slippage: number;
|
||||
mode: 'event' | 'vector';
|
||||
}
|
||||
|
||||
export interface BacktestResult {
|
||||
strategyId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
duration: number;
|
||||
initialCapital: number;
|
||||
finalCapital: number;
|
||||
totalReturn: number;
|
||||
annualizedReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
maxDrawdownDuration: number;
|
||||
winRate: number;
|
||||
totalTrades: number;
|
||||
winningTrades: number;
|
||||
losingTrades: number;
|
||||
averageWinningTrade: number;
|
||||
averageLosingTrade: number;
|
||||
profitFactor: number;
|
||||
dailyReturns: Array<{ date: Date; return: number }>;
|
||||
trades: Array<{
|
||||
symbol: string;
|
||||
entryTime: Date;
|
||||
entryPrice: number;
|
||||
exitTime: Date;
|
||||
exitPrice: number;
|
||||
quantity: number;
|
||||
pnl: number;
|
||||
pnlPercent: number;
|
||||
}>;
|
||||
// Advanced metrics
|
||||
sortinoRatio?: number;
|
||||
calmarRatio?: number;
|
||||
omegaRatio?: number;
|
||||
cagr?: number;
|
||||
volatility?: number;
|
||||
ulcerIndex?: number;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StrategyService {
|
||||
private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
// Strategy Management
|
||||
getStrategies(): Observable<ApiResponse<TradingStrategy[]>> {
|
||||
return this.http.get<ApiResponse<TradingStrategy[]>>(`${this.apiBaseUrl}/strategies`);
|
||||
}
|
||||
|
||||
getStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
|
||||
return this.http.get<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`);
|
||||
}
|
||||
|
||||
createStrategy(strategy: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> {
|
||||
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy);
|
||||
}
|
||||
|
||||
updateStrategy(
|
||||
id: string,
|
||||
updates: Partial<TradingStrategy>
|
||||
): Observable<ApiResponse<TradingStrategy>> {
|
||||
return this.http.put<ApiResponse<TradingStrategy>>(
|
||||
`${this.apiBaseUrl}/strategies/${id}`,
|
||||
updates
|
||||
);
|
||||
}
|
||||
|
||||
startStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
|
||||
return this.http.post<ApiResponse<TradingStrategy>>(
|
||||
`${this.apiBaseUrl}/strategies/${id}/start`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
|
||||
return this.http.post<ApiResponse<TradingStrategy>>(
|
||||
`${this.apiBaseUrl}/strategies/${id}/stop`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
|
||||
return this.http.post<ApiResponse<TradingStrategy>>(
|
||||
`${this.apiBaseUrl}/strategies/${id}/pause`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// Backtest Management
|
||||
getStrategyTypes(): Observable<ApiResponse<string[]>> {
|
||||
return this.http.get<ApiResponse<string[]>>(`${this.apiBaseUrl}/strategy-types`);
|
||||
}
|
||||
|
||||
getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> {
|
||||
return this.http.get<ApiResponse<Record<string, any>>>(
|
||||
`${this.apiBaseUrl}/strategy-parameters/${type}`
|
||||
);
|
||||
}
|
||||
|
||||
runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> {
|
||||
return this.http.post<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest`, request);
|
||||
}
|
||||
getBacktestResult(id: string): Observable<ApiResponse<BacktestResult>> {
|
||||
return this.http.get<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest/${id}`);
|
||||
}
|
||||
|
||||
optimizeStrategy(
|
||||
baseRequest: BacktestRequest,
|
||||
parameterGrid: Record<string, any[]>
|
||||
): Observable<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>> {
|
||||
return this.http.post<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>>(
|
||||
`${this.apiBaseUrl}/backtest/optimize`,
|
||||
{ baseRequest, parameterGrid }
|
||||
);
|
||||
}
|
||||
|
||||
// Strategy Signals and Trades
|
||||
getStrategySignals(strategyId: string): Observable<
|
||||
ApiResponse<
|
||||
Array<{
|
||||
id: string;
|
||||
strategyId: string;
|
||||
symbol: string;
|
||||
action: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
timestamp: Date;
|
||||
confidence: number;
|
||||
metadata?: any;
|
||||
}>
|
||||
>
|
||||
> {
|
||||
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`);
|
||||
}
|
||||
|
||||
getStrategyTrades(strategyId: string): Observable<
|
||||
ApiResponse<
|
||||
Array<{
|
||||
id: string;
|
||||
strategyId: string;
|
||||
symbol: string;
|
||||
entryPrice: number;
|
||||
entryTime: Date;
|
||||
exitPrice: number;
|
||||
exitTime: Date;
|
||||
quantity: number;
|
||||
pnl: number;
|
||||
pnlPercent: number;
|
||||
}>
|
||||
>
|
||||
> {
|
||||
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`);
|
||||
}
|
||||
|
||||
// Helper methods for common transformations
|
||||
formatBacktestRequest(formData: any): BacktestRequest {
|
||||
// Handle date formatting and parameter conversion
|
||||
return {
|
||||
...formData,
|
||||
startDate:
|
||||
formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate,
|
||||
endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate,
|
||||
strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams),
|
||||
};
|
||||
}
|
||||
|
||||
private convertParameterTypes(
|
||||
strategyType: string,
|
||||
params: Record<string, any>
|
||||
): Record<string, any> {
|
||||
// Convert string parameters to correct types based on strategy requirements
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (typeof value === 'string') {
|
||||
// Try to convert to number if it looks like a number
|
||||
if (!isNaN(Number(value))) {
|
||||
result[key] = Number(value);
|
||||
} else if (value.toLowerCase() === 'true') {
|
||||
result[key] = true;
|
||||
} else if (value.toLowerCase() === 'false') {
|
||||
result[key] = false;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { Injectable, signal } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: string;
|
||||
data: any;
|
||||
export interface RiskAlert {
|
||||
id: string;
|
||||
symbol: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
|
|
@ -17,199 +18,110 @@ export interface MarketDataUpdate {
|
|||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface RiskAlert {
|
||||
id: string;
|
||||
symbol: string;
|
||||
alertType: 'POSITION_LIMIT' | 'DAILY_LOSS' | 'VOLATILITY' | 'PORTFOLIO_RISK';
|
||||
message: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
timestamp: string;
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
payload?: MarketDataUpdate | RiskAlert;
|
||||
channel?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WebSocketService {
|
||||
private readonly WS_ENDPOINTS = {
|
||||
marketData: 'ws://localhost:3001/ws',
|
||||
riskGuardian: 'ws://localhost:3002/ws',
|
||||
strategyOrchestrator: 'ws://localhost:3003/ws',
|
||||
};
|
||||
private socket?: WebSocket;
|
||||
private connected = new BehaviorSubject<boolean>(false);
|
||||
private marketDataSubject = new Subject<MarketDataUpdate>();
|
||||
private riskAlertSubject = new Subject<RiskAlert>();
|
||||
|
||||
private connections = new Map<string, WebSocket>();
|
||||
private messageSubjects = new Map<string, Subject<WebSocketMessage>>();
|
||||
|
||||
// Connection status signals
|
||||
public isConnected = signal<boolean>(false);
|
||||
public connectionStatus = signal<{ [key: string]: boolean }>({
|
||||
marketData: false,
|
||||
riskGuardian: false,
|
||||
strategyOrchestrator: false,
|
||||
});
|
||||
private readonly WS_URL = 'ws://localhost:3001/ws'; // Adjust based on your backend
|
||||
|
||||
constructor() {
|
||||
this.initializeConnections();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
private initializeConnections() {
|
||||
// Initialize WebSocket connections for all services
|
||||
Object.entries(this.WS_ENDPOINTS).forEach(([service, url]) => {
|
||||
this.connect(service, url);
|
||||
});
|
||||
}
|
||||
|
||||
private connect(serviceName: string, url: string) {
|
||||
private connect() {
|
||||
try {
|
||||
const ws = new WebSocket(url);
|
||||
const messageSubject = new Subject<WebSocketMessage>();
|
||||
this.socket = new WebSocket(this.WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(`Connected to ${serviceName} WebSocket`);
|
||||
this.updateConnectionStatus(serviceName, true);
|
||||
this.socket.onopen = () => {
|
||||
this.connected.next(true);
|
||||
};
|
||||
|
||||
ws.onmessage = event => {
|
||||
this.socket.onmessage = (event) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data);
|
||||
messageSubject.next(message);
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse WebSocket message from ${serviceName}:`, error);
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
} catch {
|
||||
// Handle parsing error silently
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log(`Disconnected from ${serviceName} WebSocket`);
|
||||
this.updateConnectionStatus(serviceName, false);
|
||||
|
||||
// Attempt to reconnect after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.connect(serviceName, url);
|
||||
}, 5000);
|
||||
this.socket.onclose = () => {
|
||||
this.connected.next(false);
|
||||
// Reconnect after 5 seconds
|
||||
setTimeout(() => this.connect(), 5000);
|
||||
};
|
||||
|
||||
ws.onerror = error => {
|
||||
console.error(`WebSocket error for ${serviceName}:`, error);
|
||||
this.updateConnectionStatus(serviceName, false);
|
||||
this.socket.onerror = () => {
|
||||
this.connected.next(false);
|
||||
};
|
||||
|
||||
this.connections.set(serviceName, ws);
|
||||
this.messageSubjects.set(serviceName, messageSubject);
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to ${serviceName} WebSocket:`, error);
|
||||
this.updateConnectionStatus(serviceName, false);
|
||||
} catch {
|
||||
this.connected.next(false);
|
||||
// Retry connection after 5 seconds
|
||||
setTimeout(() => this.connect(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private updateConnectionStatus(serviceName: string, isConnected: boolean) {
|
||||
const currentStatus = this.connectionStatus();
|
||||
const newStatus = { ...currentStatus, [serviceName]: isConnected };
|
||||
this.connectionStatus.set(newStatus);
|
||||
|
||||
// Update overall connection status
|
||||
const overallConnected = Object.values(newStatus).some(status => status);
|
||||
this.isConnected.set(overallConnected);
|
||||
}
|
||||
|
||||
// Market Data Updates
|
||||
getMarketDataUpdates(): Observable<MarketDataUpdate> {
|
||||
const subject = this.messageSubjects.get('marketData');
|
||||
if (!subject) {
|
||||
throw new Error('Market data WebSocket not initialized');
|
||||
}
|
||||
|
||||
return subject.asObservable().pipe(
|
||||
filter(message => message.type === 'market_data_update'),
|
||||
map(message => message.data as MarketDataUpdate)
|
||||
);
|
||||
}
|
||||
|
||||
// Risk Alerts
|
||||
getRiskAlerts(): Observable<RiskAlert> {
|
||||
const subject = this.messageSubjects.get('riskGuardian');
|
||||
if (!subject) {
|
||||
throw new Error('Risk Guardian WebSocket not initialized');
|
||||
}
|
||||
|
||||
return subject.asObservable().pipe(
|
||||
filter(message => message.type === 'risk_alert'),
|
||||
map(message => message.data as RiskAlert)
|
||||
);
|
||||
}
|
||||
// Strategy Updates
|
||||
getStrategyUpdates(): Observable<any> {
|
||||
const subject = this.messageSubjects.get('strategyOrchestrator');
|
||||
if (!subject) {
|
||||
throw new Error('Strategy Orchestrator WebSocket not initialized');
|
||||
}
|
||||
|
||||
return subject.asObservable().pipe(
|
||||
filter(message => message.type === 'strategy_update'),
|
||||
map(message => message.data)
|
||||
);
|
||||
}
|
||||
|
||||
// Strategy Signals
|
||||
getStrategySignals(strategyId?: string): Observable<any> {
|
||||
const subject = this.messageSubjects.get('strategyOrchestrator');
|
||||
if (!subject) {
|
||||
throw new Error('Strategy Orchestrator WebSocket not initialized');
|
||||
}
|
||||
|
||||
return subject.asObservable().pipe(
|
||||
filter(
|
||||
message =>
|
||||
message.type === 'strategy_signal' &&
|
||||
(!strategyId || message.data.strategyId === strategyId)
|
||||
),
|
||||
map(message => message.data)
|
||||
);
|
||||
}
|
||||
|
||||
// Strategy Trades
|
||||
getStrategyTrades(strategyId?: string): Observable<any> {
|
||||
const subject = this.messageSubjects.get('strategyOrchestrator');
|
||||
if (!subject) {
|
||||
throw new Error('Strategy Orchestrator WebSocket not initialized');
|
||||
}
|
||||
|
||||
return subject.asObservable().pipe(
|
||||
filter(
|
||||
message =>
|
||||
message.type === 'strategy_trade' &&
|
||||
(!strategyId || message.data.strategyId === strategyId)
|
||||
),
|
||||
map(message => message.data)
|
||||
);
|
||||
}
|
||||
|
||||
// All strategy-related messages, useful for components that need all types
|
||||
getAllStrategyMessages(): Observable<WebSocketMessage> {
|
||||
const subject = this.messageSubjects.get('strategyOrchestrator');
|
||||
if (!subject) {
|
||||
throw new Error('Strategy Orchestrator WebSocket not initialized');
|
||||
}
|
||||
|
||||
return subject.asObservable().pipe(filter(message => message.type.startsWith('strategy_')));
|
||||
}
|
||||
|
||||
// Send messages
|
||||
sendMessage(serviceName: string, message: any) {
|
||||
const ws = this.connections.get(serviceName);
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn(`Cannot send message to ${serviceName}: WebSocket not connected`);
|
||||
private handleMessage(data: WebSocketMessage) {
|
||||
switch (data.type) {
|
||||
case 'marketData':
|
||||
this.marketDataSubject.next(data.payload as MarketDataUpdate);
|
||||
break;
|
||||
case 'riskAlert':
|
||||
this.riskAlertSubject.next(data.payload as RiskAlert);
|
||||
break;
|
||||
default:
|
||||
// Unknown message type - ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
disconnect() {
|
||||
this.connections.forEach((ws, _serviceName) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
public getMarketDataUpdates(): Observable<MarketDataUpdate> {
|
||||
return this.marketDataSubject.asObservable();
|
||||
}
|
||||
|
||||
public getRiskAlerts(): Observable<RiskAlert> {
|
||||
return this.riskAlertSubject.asObservable();
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.connected.value;
|
||||
}
|
||||
|
||||
public getConnectionStatus(): Observable<boolean> {
|
||||
return this.connected.asObservable();
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public subscribe(channel: string) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: channel
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(channel: string) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({
|
||||
type: 'unsubscribe',
|
||||
channel: channel
|
||||
}));
|
||||
}
|
||||
});
|
||||
this.connections.clear();
|
||||
this.messageSubjects.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* Data Service - Combined live and historical data ingestion with queue-based architecture
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { Browser } from '@stock-bot/browser';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { getLogger, shutdownLoggers } from '@stock-bot/logger';
|
||||
|
|
@ -16,6 +17,15 @@ import { exchangeRoutes, healthRoutes, queueRoutes } from './routes';
|
|||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Add CORS middleware
|
||||
app.use('*', cors({
|
||||
origin: ['http://localhost:4200', 'http://localhost:5173'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
const logger = getLogger('data-service');
|
||||
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
|
||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,26 @@ const logger = getLogger('exchange-routes');
|
|||
|
||||
export const exchangeRoutes = new Hono();
|
||||
|
||||
// Get all master exchanges
|
||||
exchangeRoutes.get('/api/exchanges', async c => {
|
||||
try {
|
||||
await connectMongoDB();
|
||||
const db = getDatabase();
|
||||
const collection = db.collection<MasterExchange>('masterExchanges');
|
||||
|
||||
const exchanges = await collection.find({}).toArray();
|
||||
|
||||
return c.json({
|
||||
data: exchanges,
|
||||
count: exchanges.length,
|
||||
status: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting all exchanges', { error });
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get master exchange details
|
||||
exchangeRoutes.get('/api/exchanges/:masterExchangeId', async c => {
|
||||
try {
|
||||
|
|
@ -91,12 +111,10 @@ exchangeRoutes.get('/api/exchanges/source/:sourceName', async c => {
|
|||
}));
|
||||
|
||||
return c.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
sourceName,
|
||||
data: exchanges,
|
||||
count: exchanges.length,
|
||||
exchanges,
|
||||
},
|
||||
status: 'success',
|
||||
sourceName,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting source exchanges', { error });
|
||||
|
|
@ -104,6 +122,45 @@ exchangeRoutes.get('/api/exchanges/source/:sourceName', async c => {
|
|||
}
|
||||
});
|
||||
|
||||
// Add source mapping to an exchange
|
||||
exchangeRoutes.post('/api/exchanges/:masterExchangeId/mappings', async c => {
|
||||
try {
|
||||
const masterExchangeId = c.req.param('masterExchangeId');
|
||||
const { sourceName, sourceData } = await c.req.json();
|
||||
|
||||
if (!sourceName || !sourceData || !sourceData.id) {
|
||||
return c.json({ error: 'sourceName and sourceData with id are required' }, 400);
|
||||
}
|
||||
|
||||
await connectMongoDB();
|
||||
const db = getDatabase();
|
||||
const collection = db.collection<MasterExchange>('masterExchanges');
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ masterExchangeId },
|
||||
{
|
||||
$set: {
|
||||
[`sourceMappings.${sourceName}`]: sourceData,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount === 0) {
|
||||
return c.json({ error: 'Exchange not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
status: 'success',
|
||||
message: 'Source mapping added successfully',
|
||||
data: { masterExchangeId, sourceName, sourceData },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error adding source mapping', { error });
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger exchange sync
|
||||
exchangeRoutes.post('/api/exchanges/sync', async c => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue