dashboard cleanup

This commit is contained in:
Boki 2025-06-15 12:50:31 -04:00
parent 660a2a1ec2
commit 56e3938561
36 changed files with 1002 additions and 3481 deletions

View file

@ -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": {

View file

@ -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' },
];

View file

@ -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',

View file

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

View file

@ -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' },

View file

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

View file

@ -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>

View file

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

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

View file

@ -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>

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

View file

@ -1 +0,0 @@
/* Portfolio specific styles */

View file

@ -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>

View file

@ -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';
}
}

View file

@ -1 +0,0 @@
/* Risk Management specific styles */

View file

@ -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>

View file

@ -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';
}
}
}

View file

@ -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',
});
}
}

View file

@ -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',
});
}
}

View file

@ -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';
}
}

View file

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

View file

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

View file

@ -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>

View file

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

View file

@ -1 +0,0 @@
/* Strategies specific styles */

View file

@ -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>

View file

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

View file

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

View file

@ -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' }} &#64;
{{ trade.entryTime | date: 'short' }}
</td>
<td class="py-2">
${{ trade.exitPrice | number: '1.2-2' }} &#64;
{{ 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>

View file

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

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

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

View file

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

View file

@ -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 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 socket?: WebSocket;
private connected = new BehaviorSubject<boolean>(false);
private marketDataSubject = new Subject<MarketDataUpdate>();
private riskAlertSubject = new Subject<RiskAlert>();
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>();
ws.onopen = () => {
console.log(`Connected to ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, true);
this.socket = new WebSocket(this.WS_URL);
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();
}
});
this.connections.clear();
this.messageSubjects.clear();
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
}));
}
}
}

View file

@ -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;

View file

@ -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({
data: exchanges,
count: exchanges.length,
status: 'success',
data: {
sourceName,
count: exchanges.length,
exchanges,
},
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 {