diff --git a/apps/dashboard/angular.json b/apps/dashboard/angular.json index 3324479..4b7618d 100644 --- a/apps/dashboard/angular.json +++ b/apps/dashboard/angular.json @@ -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": { diff --git a/apps/dashboard/src/app/app.routes.ts b/apps/dashboard/src/app/app.routes.ts index 1de8bf8..771477a 100644 --- a/apps/dashboard/src/app/app.routes.ts +++ b/apps/dashboard/src/app/app.routes.ts @@ -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' }, ]; diff --git a/apps/dashboard/src/app/app.ts b/apps/dashboard/src/app/app.ts index 2c7115f..15d7557 100644 --- a/apps/dashboard/src/app/app.ts +++ b/apps/dashboard/src/app/app.ts @@ -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', diff --git a/apps/dashboard/src/app/components/breadcrumb/breadcrumb.component.ts b/apps/dashboard/src/app/components/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000..f4bc963 --- /dev/null +++ b/apps/dashboard/src/app/components/breadcrumb/breadcrumb.component.ts @@ -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: ` + + `, + 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; + + constructor( + private breadcrumbService: BreadcrumbService, + private router: Router + ) { + this.breadcrumbs$ = this.breadcrumbService.breadcrumbs$; + } + + ngOnInit() { + // Initialize breadcrumbs for current route + this.breadcrumbService.breadcrumbs$.subscribe(); + } +} diff --git a/apps/dashboard/src/app/components/sidebar/sidebar.component.ts b/apps/dashboard/src/app/components/sidebar/sidebar.component.ts index d75344e..b42103e 100644 --- a/apps/dashboard/src/app/components/sidebar/sidebar.component.ts +++ b/apps/dashboard/src/app/components/sidebar/sidebar.component.ts @@ -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' }, diff --git a/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.css b/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.css new file mode 100644 index 0000000..e1c01b2 --- /dev/null +++ b/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.css @@ -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; +} diff --git a/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.html b/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.html new file mode 100644 index 0000000..e12e9a8 --- /dev/null +++ b/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.html @@ -0,0 +1,60 @@ +

Add Source Mapping

+ +
+ +
+

+ Add a source mapping for exchange: {{ data.masterExchangeId }} +

+ + + + Data Source + + @for (source of availableSourcesFiltered; track source.value) { + {{ source.label }} + } + + Please select a data source + + + + @if (isCustomSource) { + + Custom Source Name + + Please enter a custom source name + + } + + + + Source Exchange ID + + The unique identifier for this exchange in the source system + Please enter the source exchange ID + + + + + Display Name (Optional) + + Human-readable name for this exchange mapping + + + + + Description (Optional) + + Additional details about this exchange mapping + +
+
+ + + + + +
diff --git a/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.ts b/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.ts new file mode 100644 index 0000000..d1d850d --- /dev/null +++ b/apps/dashboard/src/app/pages/exchanges/dialogs/add-mapping-dialog.component.ts @@ -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, + @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(); + } +} diff --git a/apps/dashboard/src/app/pages/exchanges/exchanges.component.css b/apps/dashboard/src/app/pages/exchanges/exchanges.component.css new file mode 100644 index 0000000..9954d4b --- /dev/null +++ b/apps/dashboard/src/app/pages/exchanges/exchanges.component.css @@ -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; + } +} diff --git a/apps/dashboard/src/app/pages/exchanges/exchanges.component.html b/apps/dashboard/src/app/pages/exchanges/exchanges.component.html new file mode 100644 index 0000000..67fca45 --- /dev/null +++ b/apps/dashboard/src/app/pages/exchanges/exchanges.component.html @@ -0,0 +1,88 @@ + +
+ + + + +
+ +

Loading exchanges...

+
+ + + + + Master Exchanges ({{ exchanges.length }}) + + + + + + + + + + + + + + + + + + + + + + + + +
Exchange ID + {{ exchange.masterExchangeId }} + Source Mappings +
+ @if (getSourceMappingKeys(exchange.sourceMappings).length === 0) { + No mappings + } @else { + + @for (source of getSourceMappingKeys(exchange.sourceMappings); track source) { + + {{ source }} + {{ exchange.sourceMappings[source]?.id }} + + } + + } +
+
Actions + +
+ + @if (exchanges.length === 0 && !loading) { +
+ account_balance +

No exchanges found

+

Start by syncing exchanges from your data providers.

+ +
+ } +
+
+
diff --git a/apps/dashboard/src/app/pages/exchanges/exchanges.component.ts b/apps/dashboard/src/app/pages/exchanges/exchanges.component.ts new file mode 100644 index 0000000..8e7015b --- /dev/null +++ b/apps/dashboard/src/app/pages/exchanges/exchanges.component.ts @@ -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; + 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[] { + 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 }); + } + } +} diff --git a/apps/dashboard/src/app/pages/portfolio/portfolio.component.css b/apps/dashboard/src/app/pages/portfolio/portfolio.component.css deleted file mode 100644 index b74a600..0000000 --- a/apps/dashboard/src/app/pages/portfolio/portfolio.component.css +++ /dev/null @@ -1 +0,0 @@ -/* Portfolio specific styles */ diff --git a/apps/dashboard/src/app/pages/portfolio/portfolio.component.html b/apps/dashboard/src/app/pages/portfolio/portfolio.component.html deleted file mode 100644 index 91c65e8..0000000 --- a/apps/dashboard/src/app/pages/portfolio/portfolio.component.html +++ /dev/null @@ -1,203 +0,0 @@ -
- -
-
-

Portfolio

-

Manage and monitor your investment portfolio

-
- -
- - -
- -
-
-

Total Value

-

${{ portfolioSummary().totalValue.toLocaleString() }}

-
- account_balance_wallet -
-
- - -
-
-

Total P&L

-

- {{ portfolioSummary().totalPnL > 0 ? '+' : '' }}${{ portfolioSummary().totalPnL.toLocaleString() }} - ({{ portfolioSummary().totalPnLPercent.toFixed(2) }}%) -

-
- trending_up - trending_down -
-
- - -
-
-

Day Change

-

- {{ portfolioSummary().dayChange > 0 ? '+' : '' }}${{ portfolioSummary().dayChange.toLocaleString() }} - ({{ portfolioSummary().dayChangePercent.toFixed(2) }}%) -

-
- today -
-
- - -
-
-

Cash Available

-

${{ portfolioSummary().cash.toLocaleString() }}

-
- attach_money -
-
-
- - - - -
- -
-

Current Positions

-
- - -
-
- - @if (isLoading()) { -
- - Loading portfolio... -
- } @else if (error()) { -
- error -

{{ error() }}

- -
- } @else if (positions().length === 0) { -
- account_balance_wallet -

No positions found

- -
- } @else { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Symbol{{ position.symbol }}Quantity - {{ position.quantity.toLocaleString() }} - Avg Price - ${{ position.avgPrice.toFixed(2) }} - Current Price - ${{ position.currentPrice.toFixed(2) }} - Market Value - ${{ position.marketValue.toLocaleString() }} - Unrealized P&L - {{ position.unrealizedPnL > 0 ? '+' : '' }}${{ position.unrealizedPnL.toLocaleString() }} - ({{ position.unrealizedPnLPercent.toFixed(2) }}%) - Day Change - {{ position.dayChange > 0 ? '+' : '' }}${{ position.dayChange.toFixed(2) }} - ({{ position.dayChangePercent.toFixed(2) }}%) -
-
- } -
-
-
- - -
- -
- trending_up -

Performance charts and analytics will be implemented here

-
-
-
-
- - -
- -
- receipt -

Order history and management will be implemented here

-
-
-
-
-
-
diff --git a/apps/dashboard/src/app/pages/portfolio/portfolio.component.ts b/apps/dashboard/src/app/pages/portfolio/portfolio.component.ts deleted file mode 100644 index eff680d..0000000 --- a/apps/dashboard/src/app/pages/portfolio/portfolio.component.ts +++ /dev/null @@ -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({ - totalValue: 0, - totalCost: 0, - totalPnL: 0, - totalPnLPercent: 0, - dayChange: 0, - dayChangePercent: 0, - cash: 0, - positionsCount: 0, - }); - - protected positions = signal([]); - protected isLoading = signal(true); - protected error = signal(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'; - } -} diff --git a/apps/dashboard/src/app/pages/risk-management/risk-management.component.css b/apps/dashboard/src/app/pages/risk-management/risk-management.component.css deleted file mode 100644 index 7709883..0000000 --- a/apps/dashboard/src/app/pages/risk-management/risk-management.component.css +++ /dev/null @@ -1 +0,0 @@ -/* Risk Management specific styles */ diff --git a/apps/dashboard/src/app/pages/risk-management/risk-management.component.html b/apps/dashboard/src/app/pages/risk-management/risk-management.component.html deleted file mode 100644 index 2943e7b..0000000 --- a/apps/dashboard/src/app/pages/risk-management/risk-management.component.html +++ /dev/null @@ -1,178 +0,0 @@ -
- -
-
-

Risk Management

-

Monitor and control trading risks and exposure

-
- -
- - -
- @if (riskThresholds(); as thresholds) { - -
-
-

Max Position Size

-

${{ thresholds.maxPositionSize.toLocaleString() }}

-
- account_balance -
-
- - -
-
-

Max Daily Loss

-

${{ thresholds.maxDailyLoss.toLocaleString() }}

-
- trending_down -
-
- - -
-
-

Portfolio Risk Limit

-

{{ (thresholds.maxPortfolioRisk * 100).toFixed(1) }}%

-
- pie_chart -
-
- - -
-
-

Volatility Limit

-

{{ (thresholds.volatilityLimit * 100).toFixed(1) }}%

-
- show_chart -
-
- } -
- - - -
-

Risk Thresholds Configuration

-
- - @if (isLoading()) { -
- - Loading risk settings... -
- } @else if (error()) { -
- error -

{{ error() }}

- -
- } @else { -
-
- - Max Position Size ($) - - attach_money - - - - Max Daily Loss ($) - - trending_down - - - - Max Portfolio Risk (0-1) - - pie_chart - - - - Volatility Limit (0-1) - - show_chart - -
- -
- -
-
- } -
- - - -
-

Recent Risk Evaluations

-
- - @if (riskHistory().length === 0) { -
- history -

No risk evaluations found

-
- } @else { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Symbol{{ risk.symbol }}Position Value - ${{ risk.positionValue.toLocaleString() }} - Risk Level - - {{ risk.riskLevel }} - - Violations - @if (risk.violations.length > 0) { - {{ risk.violations.join(', ') }} - } @else { - None - } -
-
- } -
-
diff --git a/apps/dashboard/src/app/pages/risk-management/risk-management.component.ts b/apps/dashboard/src/app/pages/risk-management/risk-management.component.ts deleted file mode 100644 index a0a22e1..0000000 --- a/apps/dashboard/src/app/pages/risk-management/risk-management.component.ts +++ /dev/null @@ -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(null); - protected riskHistory = signal([]); - protected isLoading = signal(true); - protected isSaving = signal(false); - protected error = signal(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'; - } - } -} diff --git a/apps/dashboard/src/app/pages/strategies/components/drawdown-chart.component.ts b/apps/dashboard/src/app/pages/strategies/components/drawdown-chart.component.ts deleted file mode 100644 index 6edd138..0000000 --- a/apps/dashboard/src/app/pages/strategies/components/drawdown-chart.component.ts +++ /dev/null @@ -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: ` -
- -
- `, - 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', - }); - } -} diff --git a/apps/dashboard/src/app/pages/strategies/components/equity-chart.component.ts b/apps/dashboard/src/app/pages/strategies/components/equity-chart.component.ts deleted file mode 100644 index 72008fa..0000000 --- a/apps/dashboard/src/app/pages/strategies/components/equity-chart.component.ts +++ /dev/null @@ -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: ` -
- -
- `, - 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', - }); - } -} diff --git a/apps/dashboard/src/app/pages/strategies/components/performance-metrics.component.ts b/apps/dashboard/src/app/pages/strategies/components/performance-metrics.component.ts deleted file mode 100644 index 0e6ab69..0000000 --- a/apps/dashboard/src/app/pages/strategies/components/performance-metrics.component.ts +++ /dev/null @@ -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: ` - - - Performance Metrics - - -
-
-

Returns

-
-
-
- Total Return -
-
- {{ formatPercent(backtestResult?.totalReturn || 0) }} -
-
-
-
- Annualized Return -
-
- {{ formatPercent(backtestResult?.annualizedReturn || 0) }} -
-
-
-
CAGR
-
- {{ formatPercent(backtestResult?.cagr || 0) }} -
-
-
-
- - - -
-

Risk Metrics

-
-
-
- Max Drawdown -
-
- {{ formatPercent(backtestResult?.maxDrawdown || 0) }} -
-
-
-
- Max DD Duration -
-
- {{ formatDays(backtestResult?.maxDrawdownDuration || 0) }} -
-
-
-
- Volatility -
-
- {{ formatPercent(backtestResult?.volatility || 0) }} -
-
-
-
- Ulcer Index -
-
- {{ (backtestResult?.ulcerIndex || 0).toFixed(4) }} -
-
-
-
- - - -
-

Risk-Adjusted Returns

-
-
-
- Sharpe Ratio -
-
- {{ (backtestResult?.sharpeRatio || 0).toFixed(2) }} -
-
-
-
- Sortino Ratio -
-
- {{ (backtestResult?.sortinoRatio || 0).toFixed(2) }} -
-
-
-
- Calmar Ratio -
-
- {{ (backtestResult?.calmarRatio || 0).toFixed(2) }} -
-
-
-
- Omega Ratio -
-
- {{ (backtestResult?.omegaRatio || 0).toFixed(2) }} -
-
-
-
- - - -
-

Trade Statistics

-
-
-
Total Trades
-
- {{ backtestResult?.totalTrades || 0 }} -
-
-
-
Win Rate
-
- {{ formatPercent(backtestResult?.winRate || 0) }} -
-
-
-
Avg Win
-
- {{ formatPercent(backtestResult?.averageWinningTrade || 0) }} -
-
-
-
Avg Loss
-
- {{ formatPercent(backtestResult?.averageLosingTrade || 0) }} -
-
-
-
- Profit Factor -
-
- {{ (backtestResult?.profitFactor || 0).toFixed(2) }} -
-
-
-
-
-
-
- `, - 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'; - } -} diff --git a/apps/dashboard/src/app/pages/strategies/components/trades-table.component.ts b/apps/dashboard/src/app/pages/strategies/components/trades-table.component.ts deleted file mode 100644 index 84724a0..0000000 --- a/apps/dashboard/src/app/pages/strategies/components/trades-table.component.ts +++ /dev/null @@ -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: ` - - - Trades - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Symbol{{ trade.symbol }}Entry Time{{ formatDate(trade.entryTime) }}Entry Price{{ formatCurrency(trade.entryPrice) }}Exit Time{{ formatDate(trade.exitTime) }}Exit Price{{ formatCurrency(trade.exitPrice) }}Quantity{{ trade.quantity }}P&L - {{ formatCurrency(trade.pnl) }} - P&L % - {{ formatPercent(trade.pnlPercent) }} -
- - - -
-
- `, - 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); - } -} diff --git a/apps/dashboard/src/app/pages/strategies/dialogs/backtest-dialog.component.ts b/apps/dashboard/src/app/pages/strategies/dialogs/backtest-dialog.component.ts deleted file mode 100644 index b28dabb..0000000 --- a/apps/dashboard/src/app/pages/strategies/dialogs/backtest-dialog.component.ts +++ /dev/null @@ -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 = {}; - 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 - ) { - // 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); - } -} diff --git a/apps/dashboard/src/app/pages/strategies/dialogs/strategy-dialog.component.html b/apps/dashboard/src/app/pages/strategies/dialogs/strategy-dialog.component.html deleted file mode 100644 index 157087c..0000000 --- a/apps/dashboard/src/app/pages/strategies/dialogs/strategy-dialog.component.html +++ /dev/null @@ -1,84 +0,0 @@ -

{{isEditMode ? 'Edit Strategy' : 'Create Strategy'}}

- -
- -
- - - Strategy Name - - Name is required - - - - Description - - - - - Strategy Type - - - {{type}} - - - Strategy type is required - - - -
- -
- - {{symbol}} - cancel - -
- -
- - Add Symbol - - - -
- -
-

Suggested symbols:

-
- -
-
-
- - -
-

Strategy Parameters

-
- - {{param.key}} - - -
-
-
-
- - - - - -
diff --git a/apps/dashboard/src/app/pages/strategies/dialogs/strategy-dialog.component.ts b/apps/dashboard/src/app/pages/strategies/dialogs/strategy-dialog.component.ts deleted file mode 100644 index 59fe03e..0000000 --- a/apps/dashboard/src/app/pages/strategies/dialogs/strategy-dialog.component.ts +++ /dev/null @@ -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 = {}; - - constructor( - private fb: FormBuilder, - private strategyService: StrategyService, - @Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null, - private dialogRef: MatDialogRef - ) { - 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 = { - 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; - } -} diff --git a/apps/dashboard/src/app/pages/strategies/strategies.component.css b/apps/dashboard/src/app/pages/strategies/strategies.component.css deleted file mode 100644 index 3de2dd5..0000000 --- a/apps/dashboard/src/app/pages/strategies/strategies.component.css +++ /dev/null @@ -1 +0,0 @@ -/* Strategies specific styles */ diff --git a/apps/dashboard/src/app/pages/strategies/strategies.component.html b/apps/dashboard/src/app/pages/strategies/strategies.component.html deleted file mode 100644 index 30d948c..0000000 --- a/apps/dashboard/src/app/pages/strategies/strategies.component.html +++ /dev/null @@ -1,142 +0,0 @@ -
-
-
-

Trading Strategies

-

Configure and monitor your automated trading strategies

-
-
- - -
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Strategy -
{{strategy.name}}
-
{{strategy.description}}
-
Type{{strategy.type}}Symbols -
- - {{symbol}} - - - +{{strategy.symbols.length - 3}} more - -
-
Status -
- - {{strategy.status}} -
-
Performance -
-
- Return: - - {{strategy.performance.totalReturn | percent:'1.2-2'}} - -
-
- Win Rate: - {{strategy.performance.winRate | percent:'1.0-0'}} -
-
-
Actions -
- - - - - - - -
-
-
- - - -
- psychology -

No Strategies Yet

-

Create your first trading strategy to get started

- -
-
-
-
- - -
- -
- -
-
diff --git a/apps/dashboard/src/app/pages/strategies/strategies.component.ts b/apps/dashboard/src/app/pages/strategies/strategies.component.ts deleted file mode 100644 index ad18b2e..0000000 --- a/apps/dashboard/src/app/pages/strategies/strategies.component.ts +++ /dev/null @@ -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; - } -} diff --git a/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.css b/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.css deleted file mode 100644 index 3ea29ae..0000000 --- a/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.css +++ /dev/null @@ -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; -} diff --git a/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.html b/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.html deleted file mode 100644 index 09091c8..0000000 --- a/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.html +++ /dev/null @@ -1,249 +0,0 @@ -
-
- - -
-
-

{{ strategy.name }}

-

{{ strategy.description }}

-
-
- - - {{ strategy.status }} - -
-
- -
-
-

Type

-

{{ strategy.type }}

-
-
-

Created

-

{{ strategy.createdAt | date: 'medium' }}

-
-
-

Last Updated

-

{{ strategy.updatedAt | date: 'medium' }}

-
-
-

Symbols

-
- {{ symbol }} -
-
-
-
- - - -

Performance

-
-
-

Return

-

- {{ performance.totalReturn | percent: '1.2-2' }} -

-
-
-

Win Rate

-

{{ performance.winRate | percent: '1.0-0' }}

-
-
-

Sharpe Ratio

-

{{ performance.sharpeRatio | number: '1.2-2' }}

-
-
-

Max Drawdown

-

- {{ performance.maxDrawdown | percent: '1.2-2' }} -

-
-
-

Total Trades

-

{{ performance.totalTrades }}

-
-
-

Sortino Ratio

-

{{ performance.sortinoRatio | number: '1.2-2' }}

-
-
- - - -
- - - - -
-
-
- - - -

Strategy Parameters

-
-
-

{{ param.key }}

-

{{ param.value }}

-
-
-
- -
-

Backtest Results

- - - - - - - - - - - - -
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - -
TimeSymbolActionPriceQuantityConfidence
{{ signal.timestamp | date: 'short' }}{{ signal.symbol }} - - {{ signal.action }} - - ${{ signal.price | number: '1.2-2' }}{{ signal.quantity }}{{ signal.confidence | percent: '1.0-0' }}
-
- - - -
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - -
SymbolEntryExitQuantityP&LP&L %
{{ trade.symbol }} - ${{ trade.entryPrice | number: '1.2-2' }} @ - {{ trade.entryTime | date: 'short' }} - - ${{ trade.exitPrice | number: '1.2-2' }} @ - {{ trade.exitTime | date: 'short' }} - {{ trade.quantity }} - ${{ trade.pnl | number: '1.2-2' }} - - {{ trade.pnlPercent | number: '1.2-2' }}% -
-
- - - -
-
-
-
-
- - -
- psychology -

No strategy selected

-
-
diff --git a/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.ts b/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.ts deleted file mode 100644 index b700d9e..0000000 --- a/apps/dashboard/src/app/pages/strategies/strategy-details/strategy-details.component.ts +++ /dev/null @@ -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; - } -} diff --git a/apps/dashboard/src/app/services/breadcrumb.service.ts b/apps/dashboard/src/app/services/breadcrumb.service.ts new file mode 100644 index 0000000..483689b --- /dev/null +++ b/apps/dashboard/src/app/services/breadcrumb.service.ts @@ -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([]); + public breadcrumbs$ = this.breadcrumbsSubject.asObservable(); + + private routeLabels: Record = { + '/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; + } +} diff --git a/apps/dashboard/src/app/services/exchange.service.ts b/apps/dashboard/src/app/services/exchange.service.ts new file mode 100644 index 0000000..d9790b1 --- /dev/null +++ b/apps/dashboard/src/app/services/exchange.service.ts @@ -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; + 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; + 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 { + return this.http.get(this.baseUrl).toPromise() as Promise; + } + + async getExchange(masterExchangeId: string): Promise { + return this.http + .get(`${this.baseUrl}/${masterExchangeId}`) + .toPromise() as Promise; + } + + async addSourceMapping( + masterExchangeId: string, + sourceName: string, + sourceData: SourceMapping + ): Promise { + return this.http + .post(`${this.baseUrl}/${masterExchangeId}/mappings`, { + sourceName, + sourceData, + }) + .toPromise() as Promise; + } + + async syncExchanges(): Promise { + return this.http + .post(`${this.baseUrl}/sync`, {}) + .toPromise() as Promise; + } + + async getExchangesBySource(sourceName: string): Promise { + return this.http + .get(`${this.baseUrl}/source/${sourceName}`) + .toPromise() as Promise; + } + + async getSourceMapping(sourceName: string, sourceId: string): Promise { + return this.http + .get(`${this.baseUrl}/mapping/${sourceName}/${sourceId}`) + .toPromise() as Promise; + } +} diff --git a/apps/dashboard/src/app/services/strategy.service.ts b/apps/dashboard/src/app/services/strategy.service.ts deleted file mode 100644 index d6ca23a..0000000 --- a/apps/dashboard/src/app/services/strategy.service.ts +++ /dev/null @@ -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; - performance: { - totalTrades: number; - winRate: number; - totalReturn: number; - sharpeRatio: number; - maxDrawdown: number; - }; - createdAt: Date; - updatedAt: Date; -} - -export interface BacktestRequest { - strategyType: string; - strategyParams: Record; - 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 { - 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> { - return this.http.get>(`${this.apiBaseUrl}/strategies`); - } - - getStrategy(id: string): Observable> { - return this.http.get>(`${this.apiBaseUrl}/strategies/${id}`); - } - - createStrategy(strategy: Partial): Observable> { - return this.http.post>(`${this.apiBaseUrl}/strategies`, strategy); - } - - updateStrategy( - id: string, - updates: Partial - ): Observable> { - return this.http.put>( - `${this.apiBaseUrl}/strategies/${id}`, - updates - ); - } - - startStrategy(id: string): Observable> { - return this.http.post>( - `${this.apiBaseUrl}/strategies/${id}/start`, - {} - ); - } - - stopStrategy(id: string): Observable> { - return this.http.post>( - `${this.apiBaseUrl}/strategies/${id}/stop`, - {} - ); - } - - pauseStrategy(id: string): Observable> { - return this.http.post>( - `${this.apiBaseUrl}/strategies/${id}/pause`, - {} - ); - } - - // Backtest Management - getStrategyTypes(): Observable> { - return this.http.get>(`${this.apiBaseUrl}/strategy-types`); - } - - getStrategyParameters(type: string): Observable>> { - return this.http.get>>( - `${this.apiBaseUrl}/strategy-parameters/${type}` - ); - } - - runBacktest(request: BacktestRequest): Observable> { - return this.http.post>(`${this.apiBaseUrl}/backtest`, request); - } - getBacktestResult(id: string): Observable> { - return this.http.get>(`${this.apiBaseUrl}/backtest/${id}`); - } - - optimizeStrategy( - baseRequest: BacktestRequest, - parameterGrid: Record - ): Observable }>>> { - return this.http.post }>>>( - `${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>(`${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>(`${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 - ): Record { - // Convert string parameters to correct types based on strategy requirements - const result: Record = {}; - - 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; - } -} diff --git a/apps/dashboard/src/app/services/websocket.service.ts b/apps/dashboard/src/app/services/websocket.service.ts index 00a0cc3..7f5970e 100644 --- a/apps/dashboard/src/app/services/websocket.service.ts +++ b/apps/dashboard/src/app/services/websocket.service.ts @@ -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(); - private messageSubjects = new Map>(); - - // Connection status signals - public isConnected = signal(false); - public connectionStatus = signal<{ [key: string]: boolean }>({ - marketData: false, - riskGuardian: false, - strategyOrchestrator: false, - }); + private socket?: WebSocket; + private connected = new BehaviorSubject(false); + private marketDataSubject = new Subject(); + private riskAlertSubject = new Subject(); + + 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(); - - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { + return this.marketDataSubject.asObservable(); + } + + public getRiskAlerts(): Observable { + return this.riskAlertSubject.asObservable(); + } + + public isConnected(): boolean { + return this.connected.value; + } + + public getConnectionStatus(): Observable { + 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 + })); + } } } diff --git a/apps/data-service/src/index.ts b/apps/data-service/src/index.ts index 92a09a6..caa91fc 100644 --- a/apps/data-service/src/index.ts +++ b/apps/data-service/src/index.ts @@ -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 | null = null; diff --git a/apps/data-service/src/routes/exchange.routes.ts b/apps/data-service/src/routes/exchange.routes.ts index de96151..081c54b 100644 --- a/apps/data-service/src/routes/exchange.routes.ts +++ b/apps/data-service/src/routes/exchange.routes.ts @@ -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('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('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 {