diff --git a/src/analytics/dto/bridge-performance-metric.dto.ts b/src/analytics/dto/bridge-performance-metric.dto.ts new file mode 100644 index 0000000..7a72713 --- /dev/null +++ b/src/analytics/dto/bridge-performance-metric.dto.ts @@ -0,0 +1,284 @@ +import { IsOptional, IsString, IsEnum, IsDateString, IsInt, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +/** + * Time interval type + */ +export type TimeInterval = 'hourly' | 'daily' | 'weekly' | 'monthly'; + +/** + * Time interval enum for validation + */ +export enum TimeIntervalEnum { + HOURLY = 'hourly', + DAILY = 'daily', + WEEKLY = 'weekly', + MONTHLY = 'monthly', +} + +/** + * DTO for querying historical performance metrics + */ +export class BridgePerformanceMetricQueryDto { + @ApiPropertyOptional({ description: 'Filter by bridge name' }) + @IsOptional() + @IsString() + bridgeName?: string; + + @ApiPropertyOptional({ description: 'Filter by source chain' }) + @IsOptional() + @IsString() + sourceChain?: string; + + @ApiPropertyOptional({ description: 'Filter by destination chain' }) + @IsOptional() + @IsString() + destinationChain?: string; + + @ApiPropertyOptional({ description: 'Filter by token' }) + @IsOptional() + @IsString() + token?: string; + + @ApiPropertyOptional({ + description: 'Time interval for aggregation', + enum: TimeIntervalEnum, + default: TimeIntervalEnum.DAILY, + }) + @IsOptional() + @IsEnum(TimeIntervalEnum) + timeInterval?: TimeInterval = TimeIntervalEnum.DAILY; + + @ApiPropertyOptional({ description: 'Start date for time range (ISO 8601)' }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ description: 'End date for time range (ISO 8601)' }) + @IsOptional() + @IsDateString() + endDate?: string; + + @ApiPropertyOptional({ description: 'Page number for pagination', default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ description: 'Items per page', default: 50 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + limit?: number = 50; +} + +/** + * DTO for a single performance metric data point + */ +export class BridgePerformanceMetricDto { + @ApiProperty({ description: 'Bridge name' }) + bridgeName: string; + + @ApiProperty({ description: 'Source chain' }) + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + destinationChain: string; + + @ApiPropertyOptional({ description: 'Token symbol' }) + token?: string; + + @ApiProperty({ description: 'Time interval', enum: TimeIntervalEnum }) + timeInterval: TimeInterval; + + @ApiProperty({ description: 'Timestamp for this metric period' }) + timestamp: Date; + + @ApiProperty({ description: 'Total transfers in this period' }) + totalTransfers: number; + + @ApiProperty({ description: 'Successful transfers' }) + successfulTransfers: number; + + @ApiProperty({ description: 'Failed transfers' }) + failedTransfers: number; + + @ApiProperty({ description: 'Success rate percentage' }) + successRate: number; + + @ApiProperty({ description: 'Failure rate percentage' }) + failureRate: number; + + @ApiPropertyOptional({ description: 'Average settlement time in milliseconds' }) + averageSettlementTimeMs?: number; + + @ApiPropertyOptional({ description: 'Minimum settlement time' }) + minSettlementTimeMs?: number; + + @ApiPropertyOptional({ description: 'Maximum settlement time' }) + maxSettlementTimeMs?: number; + + @ApiPropertyOptional({ description: 'Average fee amount' }) + averageFee?: number; + + @ApiPropertyOptional({ description: 'Minimum fee' }) + minFee?: number; + + @ApiPropertyOptional({ description: 'Maximum fee' }) + maxFee?: number; + + @ApiPropertyOptional({ description: 'Average slippage percentage' }) + averageSlippagePercent?: number; + + @ApiPropertyOptional({ description: 'Minimum slippage' }) + minSlippagePercent?: number; + + @ApiPropertyOptional({ description: 'Maximum slippage' }) + maxSlippagePercent?: number; + + @ApiProperty({ description: 'Total volume transferred' }) + totalVolume: number; + + @ApiProperty({ description: 'Total fees collected' }) + totalFees: number; +} + +/** + * DTO for paginated performance metrics response + */ +export class BridgePerformanceMetricResponseDto { + @ApiProperty({ description: 'Performance metrics data', type: [BridgePerformanceMetricDto] }) + data: BridgePerformanceMetricDto[]; + + @ApiProperty({ description: 'Total number of records' }) + total: number; + + @ApiProperty({ description: 'Current page number' }) + page: number; + + @ApiProperty({ description: 'Items per page' }) + limit: number; + + @ApiProperty({ description: 'Total number of pages' }) + totalPages: number; + + @ApiProperty({ description: 'Time interval used' }) + timeInterval: TimeInterval; + + @ApiProperty({ description: 'Response generation timestamp' }) + generatedAt: Date; +} + +/** + * DTO for historical trends data + */ +export class HistoricalTrendsDto { + @ApiProperty({ description: 'Bridge name' }) + bridgeName: string; + + @ApiProperty({ description: 'Source chain' }) + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + destinationChain: string; + + @ApiPropertyOptional({ description: 'Token symbol' }) + token?: string; + + @ApiProperty({ description: 'Time interval', enum: TimeIntervalEnum }) + timeInterval: TimeInterval; + + @ApiProperty({ description: 'Trend data points', type: [BridgePerformanceMetricDto] }) + trends: BridgePerformanceMetricDto[]; + + @ApiProperty({ description: 'Response generation timestamp' }) + generatedAt: Date; +} + +/** + * DTO for performance comparison between bridges + */ +export class BridgePerformanceComparisonDto { + @ApiProperty({ description: 'Bridge name' }) + bridgeName: string; + + @ApiProperty({ description: 'Source chain' }) + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + destinationChain: string; + + @ApiProperty({ description: 'Time interval', enum: TimeIntervalEnum }) + timeInterval: TimeInterval; + + @ApiProperty({ description: 'Number of data points' }) + dataPoints: number; + + @ApiProperty({ description: 'Average success rate over period' }) + avgSuccessRate: number; + + @ApiProperty({ description: 'Average settlement time over period' }) + avgSettlementTimeMs: number; + + @ApiProperty({ description: 'Average fee over period' }) + avgFee: number; + + @ApiProperty({ description: 'Average slippage over period' }) + avgSlippagePercent: number; + + @ApiProperty({ description: 'Total volume over period' }) + totalVolume: number; + + @ApiProperty({ description: 'Total transfers over period' }) + totalTransfers: number; + + @ApiProperty({ description: 'Trend direction (improving/declining/stable)' }) + trendDirection: 'improving' | 'declining' | 'stable'; +} + +/** + * DTO for performance comparison response + */ +export class BridgePerformanceComparisonResponseDto { + @ApiProperty({ description: 'Comparison data', type: [BridgePerformanceComparisonDto] }) + comparisons: BridgePerformanceComparisonDto[]; + + @ApiProperty({ description: 'Time interval used' }) + timeInterval: TimeInterval; + + @ApiProperty({ description: 'Start date of comparison period' }) + startDate: Date; + + @ApiProperty({ description: 'End date of comparison period' }) + endDate: Date; + + @ApiProperty({ description: 'Response generation timestamp' }) + generatedAt: Date; +} + +/** + * DTO for aggregation trigger request + */ +export class TriggerAggregationDto { + @ApiPropertyOptional({ + description: 'Time interval to aggregate', + enum: TimeInterval, + default: TimeInterval.DAILY, + }) + @IsOptional() + @IsEnum(TimeInterval) + timeInterval?: TimeInterval = TimeInterval.DAILY; + + @ApiPropertyOptional({ description: 'Date to aggregate (defaults to previous period)' }) + @IsOptional() + @IsDateString() + date?: string; + + @ApiPropertyOptional({ description: 'Specific bridge to aggregate' }) + @IsOptional() + @IsString() + bridgeName?: string; +} diff --git a/src/analytics/entities/bridge-performance-metric.entity.ts b/src/analytics/entities/bridge-performance-metric.entity.ts new file mode 100644 index 0000000..b104e04 --- /dev/null +++ b/src/analytics/entities/bridge-performance-metric.entity.ts @@ -0,0 +1,109 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * Time interval for historical metrics aggregation + */ +export type TimeInterval = 'hourly' | 'daily' | 'weekly' | 'monthly'; + +/** + * BridgePerformanceMetric Entity + * + * Stores historical performance metrics for bridge routes over time. + * Supports multiple time intervals for flexible analysis. + */ +@Entity('bridge_performance_metrics') +@Index(['bridgeName', 'sourceChain', 'destinationChain', 'timeInterval', 'timestamp']) +@Index(['timeInterval', 'timestamp']) +export class BridgePerformanceMetric { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'bridge_name' }) + bridgeName: string; + + @Column({ name: 'source_chain' }) + sourceChain: string; + + @Column({ name: 'destination_chain' }) + destinationChain: string; + + @Column({ name: 'token', nullable: true }) + token: string | null; + + @Column({ + name: 'time_interval', + type: 'enum', + enum: ['hourly', 'daily', 'weekly', 'monthly'], + }) + timeInterval: TimeInterval; + + @Column({ name: 'total_transfers', type: 'int', default: 0 }) + totalTransfers: number; + + @Column({ name: 'successful_transfers', type: 'int', default: 0 }) + successfulTransfers: number; + + @Column({ name: 'failed_transfers', type: 'int', default: 0 }) + failedTransfers: number; + + @Column({ name: 'average_settlement_time_ms', type: 'bigint', nullable: true }) + averageSettlementTimeMs: number | null; + + @Column({ name: 'min_settlement_time_ms', type: 'bigint', nullable: true }) + minSettlementTimeMs: number | null; + + @Column({ name: 'max_settlement_time_ms', type: 'bigint', nullable: true }) + maxSettlementTimeMs: number | null; + + @Column({ name: 'average_fee', type: 'decimal', precision: 30, scale: 10, nullable: true }) + averageFee: number | null; + + @Column({ name: 'min_fee', type: 'decimal', precision: 30, scale: 10, nullable: true }) + minFee: number | null; + + @Column({ name: 'max_fee', type: 'decimal', precision: 30, scale: 10, nullable: true }) + maxFee: number | null; + + @Column({ name: 'average_slippage_percent', type: 'decimal', precision: 10, scale: 4, nullable: true }) + averageSlippagePercent: number | null; + + @Column({ name: 'min_slippage_percent', type: 'decimal', precision: 10, scale: 4, nullable: true }) + minSlippagePercent: number | null; + + @Column({ name: 'max_slippage_percent', type: 'decimal', precision: 10, scale: 4, nullable: true }) + maxSlippagePercent: number | null; + + @Column({ name: 'total_volume', type: 'decimal', precision: 30, scale: 10, default: 0 }) + totalVolume: number; + + @Column({ name: 'total_fees', type: 'decimal', precision: 30, scale: 10, default: 0 }) + totalFees: number; + + @Column({ type: 'timestamptz' }) + timestamp: Date; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + /** + * Computed success rate percentage + */ + get successRate(): number { + if (this.totalTransfers === 0) return 0; + return (this.successfulTransfers / this.totalTransfers) * 100; + } + + /** + * Computed failure rate percentage + */ + get failureRate(): number { + if (this.totalTransfers === 0) return 0; + return (this.failedTransfers / this.totalTransfers) * 100; + } +} diff --git a/src/analytics/performance-metric.service.ts b/src/analytics/performance-metric.service.ts new file mode 100644 index 0000000..587a80d --- /dev/null +++ b/src/analytics/performance-metric.service.ts @@ -0,0 +1,586 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Between, LessThan, FindOptionsWhere } from 'typeorm'; +import { + BridgePerformanceMetric, + TimeInterval, +} from './entities/bridge-performance-metric.entity'; +import { + BridgePerformanceMetricQueryDto, + BridgePerformanceMetricResponseDto, + BridgePerformanceMetricDto, + HistoricalTrendsDto, + BridgePerformanceComparisonResponseDto, + BridgePerformanceComparisonDto, +} from './dto/bridge-performance-metric.dto'; +import { BridgeBenchmark } from '../bridge-benchmark/entities/bridge-benchmark.entity'; + +/** + * Performance Metric Service + * + * Manages historical performance metrics aggregation and retrieval. + * Provides time-series data for trend analysis and bridge comparisons. + */ +@Injectable() +export class PerformanceMetricService { + private readonly logger = new Logger(PerformanceMetricService.name); + + constructor( + @InjectRepository(BridgePerformanceMetric) + private readonly metricRepository: Repository, + @InjectRepository(BridgeBenchmark) + private readonly benchmarkRepository: Repository, + ) {} + + /** + * Get paginated performance metrics with optional filters + */ + async getMetrics( + query: BridgePerformanceMetricQueryDto, + ): Promise { + const where: FindOptionsWhere = { + timeInterval: query.timeInterval, + }; + + if (query.bridgeName) { + where.bridgeName = query.bridgeName; + } + if (query.sourceChain) { + where.sourceChain = query.sourceChain; + } + if (query.destinationChain) { + where.destinationChain = query.destinationChain; + } + if (query.token) { + where.token = query.token; + } + if (query.startDate && query.endDate) { + where.timestamp = Between( + new Date(query.startDate), + new Date(query.endDate), + ); + } + + const [data, total] = await this.metricRepository.findAndCount({ + where, + order: { timestamp: 'DESC' }, + skip: (query.page - 1) * query.limit, + take: query.limit, + }); + + const metrics: BridgePerformanceMetricDto[] = data.map((entity) => + this.mapToMetricDto(entity), + ); + + return { + data: metrics, + total, + page: query.page, + limit: query.limit, + totalPages: Math.ceil(total / query.limit), + timeInterval: query.timeInterval, + generatedAt: new Date(), + }; + } + + /** + * Get historical trends for a specific route + */ + async getHistoricalTrends( + bridgeName: string, + sourceChain: string, + destinationChain: string, + timeInterval: TimeInterval, + startDate: Date, + endDate: Date, + token?: string, + ): Promise { + const where: FindOptionsWhere = { + bridgeName, + sourceChain, + destinationChain, + timeInterval, + timestamp: Between(startDate, endDate), + }; + + if (token) { + where.token = token; + } + + const data = await this.metricRepository.find({ + where, + order: { timestamp: 'ASC' }, + }); + + const trends: BridgePerformanceMetricDto[] = data.map((entity) => + this.mapToMetricDto(entity), + ); + + return { + bridgeName, + sourceChain, + destinationChain, + token, + timeInterval, + trends, + generatedAt: new Date(), + }; + } + + /** + * Compare performance across multiple bridges + */ + async compareBridgePerformance( + bridgeNames: string[], + timeInterval: TimeInterval, + startDate: Date, + endDate: Date, + sourceChain?: string, + destinationChain?: string, + token?: string, + ): Promise { + const comparisons: BridgePerformanceComparisonDto[] = []; + + for (const bridgeName of bridgeNames) { + const where: FindOptionsWhere = { + bridgeName, + timeInterval, + timestamp: Between(startDate, endDate), + }; + + if (sourceChain) where.sourceChain = sourceChain; + if (destinationChain) where.destinationChain = destinationChain; + if (token) where.token = token; + + const metrics = await this.metricRepository.find({ where }); + + if (metrics.length === 0) continue; + + // Aggregate across all routes for this bridge + const totalTransfers = metrics.reduce( + (sum, m) => sum + m.totalTransfers, + 0, + ); + const successfulTransfers = metrics.reduce( + (sum, m) => sum + m.successfulTransfers, + 0, + ); + + const avgSuccessRate = + totalTransfers > 0 ? (successfulTransfers / totalTransfers) * 100 : 0; + + const settlementTimes = metrics + .filter((m) => m.averageSettlementTimeMs) + .map((m) => m.averageSettlementTimeMs as number); + const avgSettlementTimeMs = + settlementTimes.length > 0 + ? settlementTimes.reduce((a, b) => a + b, 0) / settlementTimes.length + : 0; + + const fees = metrics + .filter((m) => m.averageFee) + .map((m) => m.averageFee as number); + const avgFee = + fees.length > 0 ? fees.reduce((a, b) => a + b, 0) / fees.length : 0; + + const slippages = metrics + .filter((m) => m.averageSlippagePercent) + .map((m) => m.averageSlippagePercent as number); + const avgSlippagePercent = + slippages.length > 0 + ? slippages.reduce((a, b) => a + b, 0) / slippages.length + : 0; + + const totalVolume = metrics.reduce((sum, m) => sum + m.totalVolume, 0); + + // Determine trend direction based on success rate changes + const sortedByTime = [...metrics].sort( + (a, b) => a.timestamp.getTime() - b.timestamp.getTime(), + ); + let trendDirection: 'improving' | 'declining' | 'stable' = 'stable'; + if (sortedByTime.length >= 2) { + const firstHalf = sortedByTime.slice(0, Math.floor(sortedByTime.length / 2)); + const secondHalf = sortedByTime.slice(Math.floor(sortedByTime.length / 2)); + + const firstRate = firstHalf.reduce((sum, m) => sum + m.successRate, 0) / firstHalf.length; + const secondRate = secondHalf.reduce((sum, m) => sum + m.successRate, 0) / secondHalf.length; + + if (secondRate > firstRate + 2) trendDirection = 'improving'; + else if (secondRate < firstRate - 2) trendDirection = 'declining'; + } + + comparisons.push({ + bridgeName, + sourceChain: sourceChain || 'all', + destinationChain: destinationChain || 'all', + timeInterval, + dataPoints: metrics.length, + avgSuccessRate, + avgSettlementTimeMs, + avgFee, + avgSlippagePercent, + totalVolume, + totalTransfers, + trendDirection, + }); + } + + return { + comparisons, + timeInterval, + startDate, + endDate, + generatedAt: new Date(), + }; + } + + /** + * Aggregate metrics for a specific time period + */ + async aggregateMetrics( + timeInterval: TimeInterval, + date: Date, + bridgeName?: string, + ): Promise<{ processed: number; inserted: number }> { + const { startTime, endTime } = this.getTimeRange(timeInterval, date); + + this.logger.log( + `Aggregating ${timeInterval} metrics for ${date.toISOString()}${ + bridgeName ? ` (${bridgeName})` : '' + }`, + ); + + // Build query for raw benchmark data + let query = this.benchmarkRepository + .createQueryBuilder('b') + .where('b.created_at >= :startTime', { startTime }) + .andWhere('b.created_at < :endTime', { endTime }); + + if (bridgeName) { + query = query.andWhere('b.bridge_name = :bridgeName', { bridgeName }); + } + + const benchmarks = await query.getMany(); + + if (benchmarks.length === 0) { + this.logger.debug(`No benchmarks found for period`); + return { processed: 0, inserted: 0 }; + } + + // Group by route + const grouped = this.groupByRoute(benchmarks); + let inserted = 0; + + for (const [key, group] of grouped.entries()) { + const metric = this.calculateMetrics(group, timeInterval, startTime); + + // Check for existing metric + const existing = await this.metricRepository.findOne({ + where: { + bridgeName: metric.bridgeName, + sourceChain: metric.sourceChain, + destinationChain: metric.destinationChain, + token: metric.token, + timeInterval, + timestamp: startTime, + }, + }); + + if (existing) { + // Update existing + Object.assign(existing, metric); + await this.metricRepository.save(existing); + } else { + // Create new + const entity = this.metricRepository.create(metric); + await this.metricRepository.save(entity); + inserted++; + } + } + + this.logger.log( + `Aggregated ${benchmarks.length} benchmarks into ${inserted} metrics`, + ); + return { processed: benchmarks.length, inserted }; + } + + /** + * Run aggregation for all bridges for a date range + */ + async aggregateDateRange( + timeInterval: TimeInterval, + startDate: Date, + endDate: Date, + ): Promise<{ processed: number; inserted: number }> { + let totalProcessed = 0; + let totalInserted = 0; + + const current = new Date(startDate); + while (current < endDate) { + const result = await this.aggregateMetrics(timeInterval, new Date(current)); + totalProcessed += result.processed; + totalInserted += result.inserted; + + // Advance to next period + switch (timeInterval) { + case 'hourly': + current.setHours(current.getHours() + 1); + break; + case 'daily': + current.setDate(current.getDate() + 1); + break; + case 'weekly': + current.setDate(current.getDate() + 7); + break; + case 'monthly': + current.setMonth(current.getMonth() + 1); + break; + } + } + + return { processed: totalProcessed, inserted: totalInserted }; + } + + /** + * Get the latest aggregated metrics + */ + async getLatestMetrics( + timeInterval: TimeInterval, + limit = 10, + ): Promise { + return this.metricRepository.find({ + where: { timeInterval }, + order: { timestamp: 'DESC' }, + take: limit, + }); + } + + /** + * Get metric summary for a route + */ + async getMetricSummary( + bridgeName: string, + sourceChain: string, + destinationChain: string, + token?: string, + ): Promise<{ + totalDataPoints: number; + dateRange: { start: Date | null; end: Date | null }; + overallStats: { + totalTransfers: number; + avgSuccessRate: number; + avgSettlementTimeMs: number; + avgFee: number; + avgSlippagePercent: number; + totalVolume: number; + }; + } | null> { + const where: FindOptionsWhere = { + bridgeName, + sourceChain, + destinationChain, + }; + if (token) where.token = token; + + const metrics = await this.metricRepository.find({ where }); + + if (metrics.length === 0) return null; + + const timestamps = metrics.map((m) => m.timestamp); + const totalTransfers = metrics.reduce((sum, m) => sum + m.totalTransfers, 0); + const successfulTransfers = metrics.reduce( + (sum, m) => sum + m.successfulTransfers, + 0, + ); + + const settlementTimes = metrics + .filter((m) => m.averageSettlementTimeMs) + .map((m) => m.averageSettlementTimeMs as number); + const fees = metrics.filter((m) => m.averageFee).map((m) => m.averageFee as number); + const slippages = metrics + .filter((m) => m.averageSlippagePercent) + .map((m) => m.averageSlippagePercent as number); + + return { + totalDataPoints: metrics.length, + dateRange: { + start: new Date(Math.min(...timestamps.map((t) => t.getTime()))), + end: new Date(Math.max(...timestamps.map((t) => t.getTime()))), + }, + overallStats: { + totalTransfers, + avgSuccessRate: + totalTransfers > 0 ? (successfulTransfers / totalTransfers) * 100 : 0, + avgSettlementTimeMs: + settlementTimes.length > 0 + ? settlementTimes.reduce((a, b) => a + b, 0) / settlementTimes.length + : 0, + avgFee: + fees.length > 0 ? fees.reduce((a, b) => a + b, 0) / fees.length : 0, + avgSlippagePercent: + slippages.length > 0 + ? slippages.reduce((a, b) => a + b, 0) / slippages.length + : 0, + totalVolume: metrics.reduce((sum, m) => sum + m.totalVolume, 0), + }, + }; + } + + /** + * Delete old metrics to save storage + */ + async pruneOldMetrics( + timeInterval: TimeInterval, + olderThan: Date, + ): Promise { + const result = await this.metricRepository.delete({ + timeInterval, + timestamp: LessThan(olderThan), + }); + return result.affected || 0; + } + + /** + * Group benchmarks by route + */ + private groupByRoute( + benchmarks: BridgeBenchmark[], + ): Map { + const grouped = new Map(); + + for (const benchmark of benchmarks) { + const key = `${benchmark.bridgeName}:${benchmark.sourceChain}:${benchmark.destinationChain}:${benchmark.token || 'null'}`; + if (!grouped.has(key)) { + grouped.set(key, []); + } + grouped.get(key)!.push(benchmark); + } + + return grouped; + } + + /** + * Calculate metrics from a group of benchmarks + */ + private calculateMetrics( + benchmarks: BridgeBenchmark[], + timeInterval: TimeInterval, + timestamp: Date, + ): Partial { + const first = benchmarks[0]; + const totalTransfers = benchmarks.length; + const successful = benchmarks.filter( + (b) => b.status === 'confirmed', + ).length; + const failed = benchmarks.filter((b) => b.status === 'failed').length; + + // Settlement times + const settlementTimes = benchmarks + .filter((b) => b.durationMs && b.status === 'confirmed') + .map((b) => b.durationMs as number); + + // Amounts for volume + const amounts = benchmarks + .filter((b) => b.amount) + .map((b) => parseFloat(b.amount?.toString() || '0')); + + return { + bridgeName: first.bridgeName, + sourceChain: first.sourceChain, + destinationChain: first.destinationChain, + token: first.token, + timeInterval, + timestamp, + totalTransfers, + successfulTransfers: successful, + failedTransfers: failed, + averageSettlementTimeMs: + settlementTimes.length > 0 + ? settlementTimes.reduce((a, b) => a + b, 0) / settlementTimes.length + : null, + minSettlementTimeMs: + settlementTimes.length > 0 ? Math.min(...settlementTimes) : null, + maxSettlementTimeMs: + settlementTimes.length > 0 ? Math.max(...settlementTimes) : null, + totalVolume: amounts.reduce((a, b) => a + b, 0), + // Fees and slippage would come from additional data sources + averageFee: null, + minFee: null, + maxFee: null, + averageSlippagePercent: null, + minSlippagePercent: null, + maxSlippagePercent: null, + totalFees: 0, + }; + } + + /** + * Get time range for a time interval + */ + private getTimeRange( + timeInterval: TimeInterval, + date: Date, + ): { startTime: Date; endTime: Date } { + const start = new Date(date); + const end = new Date(date); + + switch (timeInterval) { + case 'hourly': + start.setMinutes(0, 0, 0); + end.setMinutes(0, 0, 0); + end.setHours(end.getHours() + 1); + break; + case 'daily': + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + end.setDate(end.getDate() + 1); + break; + case 'weekly': + // Start of week (Sunday) + const dayOfWeek = start.getDay(); + start.setDate(start.getDate() - dayOfWeek); + start.setHours(0, 0, 0, 0); + end.setTime(start.getTime()); + end.setDate(end.getDate() + 7); + break; + case 'monthly': + start.setDate(1); + start.setHours(0, 0, 0, 0); + end.setTime(start.getTime()); + end.setMonth(end.getMonth() + 1); + break; + } + + return { startTime: start, endTime: end }; + } + + /** + * Map entity to DTO + */ + private mapToMetricDto(entity: BridgePerformanceMetric): BridgePerformanceMetricDto { + return { + bridgeName: entity.bridgeName, + sourceChain: entity.sourceChain, + destinationChain: entity.destinationChain, + token: entity.token || undefined, + timeInterval: entity.timeInterval, + timestamp: entity.timestamp, + totalTransfers: entity.totalTransfers, + successfulTransfers: entity.successfulTransfers, + failedTransfers: entity.failedTransfers, + successRate: entity.successRate, + failureRate: entity.failureRate, + averageSettlementTimeMs: entity.averageSettlementTimeMs || undefined, + minSettlementTimeMs: entity.minSettlementTimeMs || undefined, + maxSettlementTimeMs: entity.maxSettlementTimeMs || undefined, + averageFee: entity.averageFee || undefined, + minFee: entity.minFee || undefined, + maxFee: entity.maxFee || undefined, + averageSlippagePercent: entity.averageSlippagePercent || undefined, + minSlippagePercent: entity.minSlippagePercent || undefined, + maxSlippagePercent: entity.maxSlippagePercent || undefined, + totalVolume: entity.totalVolume, + totalFees: entity.totalFees, + }; + } +} diff --git a/src/analytics/types/performance-metrics.types.ts b/src/analytics/types/performance-metrics.types.ts new file mode 100644 index 0000000..e2ec93f --- /dev/null +++ b/src/analytics/types/performance-metrics.types.ts @@ -0,0 +1,231 @@ +/** + * Historical Bridge Performance Metrics Types + * + * TypeScript interfaces for historical performance tracking + */ + +/** + * Time interval for metric aggregation + */ +export type TimeInterval = 'hourly' | 'daily' | 'weekly' | 'monthly'; + +/** + * Historical performance metric interface + */ +export interface BridgePerformanceMetric { + id: string; + bridgeName: string; + sourceChain: string; + destinationChain: string; + token: string | null; + timeInterval: TimeInterval; + totalTransfers: number; + successfulTransfers: number; + failedTransfers: number; + averageSettlementTimeMs: number | null; + minSettlementTimeMs: number | null; + maxSettlementTimeMs: number | null; + averageFee: number | null; + minFee: number | null; + maxFee: number | null; + averageSlippagePercent: number | null; + minSlippagePercent: number | null; + maxSlippagePercent: number | null; + totalVolume: number; + totalFees: number; + timestamp: Date; + createdAt: Date; + successRate: number; + failureRate: number; +} + +/** + * Options for useBridgePerformanceMetrics hook + */ +export interface UseBridgePerformanceMetricsOptions { + /** Filter by bridge name */ + bridgeName?: string; + /** Filter by source chain */ + sourceChain?: string; + /** Filter by destination chain */ + destinationChain?: string; + /** Filter by token */ + token?: string; + /** Time interval for aggregation */ + timeInterval?: TimeInterval; + /** Start date for time range */ + startDate?: Date; + /** End date for time range */ + endDate?: Date; + /** Page number for pagination */ + page?: number; + /** Items per page */ + limit?: number; + /** Auto-refresh interval in milliseconds (0 to disable) */ + refreshInterval?: number; + /** Enable/disable the query */ + enabled?: boolean; +} + +/** + * Result returned by useBridgePerformanceMetrics hook + */ +export interface UseBridgePerformanceMetricsResult { + /** Performance metrics data array */ + metrics: BridgePerformanceMetric[]; + /** Loading state */ + loading: boolean; + /** Error if any */ + error: Error | null; + /** Total count for pagination */ + total: number; + /** Current page */ + page: number; + /** Total pages */ + totalPages: number; + /** Time interval used */ + timeInterval: TimeInterval; + /** Refetch function to manually refresh data */ + refetch: () => Promise; +} + +/** + * Options for historical trends + */ +export interface UseHistoricalTrendsOptions { + bridgeName: string; + sourceChain: string; + destinationChain: string; + token?: string; + timeInterval: TimeInterval; + startDate: Date; + endDate: Date; +} + +/** + * Result for historical trends + */ +export interface UseHistoricalTrendsResult { + /** Trend data points */ + trends: BridgePerformanceMetric[]; + /** Loading state */ + loading: boolean; + /** Error if any */ + error: Error | null; + /** Refetch function */ + refetch: () => Promise; +} + +/** + * Performance trend analysis + */ +export interface PerformanceTrendAnalysis { + /** Metric being analyzed */ + metric: 'successRate' | 'settlementTime' | 'fees' | 'slippage' | 'volume'; + /** Trend direction */ + direction: 'improving' | 'declining' | 'stable'; + /** Percentage change over period */ + changePercent: number; + /** Starting value */ + startValue: number; + /** Ending value */ + endValue: number; + /** Average value over period */ + averageValue: number; + /** Minimum value */ + minValue: number; + /** Maximum value */ + maxValue: number; +} + +/** + * Bridge comparison data + */ +export interface BridgeComparison { + bridgeName: string; + sourceChain: string; + destinationChain: string; + timeInterval: TimeInterval; + dataPoints: number; + avgSuccessRate: number; + avgSettlementTimeMs: number; + avgFee: number; + avgSlippagePercent: number; + totalVolume: number; + totalTransfers: number; + trendDirection: 'improving' | 'declining' | 'stable'; + trendAnalysis: PerformanceTrendAnalysis[]; +} + +/** + * Options for bridge comparison + */ +export interface UseBridgeComparisonOptions { + bridgeNames?: string[]; + sourceChain?: string; + destinationChain?: string; + token?: string; + timeInterval: TimeInterval; + startDate: Date; + endDate: Date; +} + +/** + * Result for bridge comparison + */ +export interface UseBridgeComparisonResult { + /** Comparison data */ + comparisons: BridgeComparison[]; + /** Loading state */ + loading: boolean; + /** Error if any */ + error: Error | null; + /** Refetch function */ + refetch: () => Promise; +} + +/** + * Aggregation job status + */ +export interface AggregationJobStatus { + id: string; + timeInterval: TimeInterval; + date: Date; + status: 'pending' | 'running' | 'completed' | 'failed'; + progress: number; + recordsProcessed: number; + recordsInserted: number; + error?: string; + startedAt?: Date; + completedAt?: Date; +} + +/** + * Metric summary for quick overview + */ +export interface PerformanceMetricSummary { + bridgeName: string; + sourceChain: string; + destinationChain: string; + totalDataPoints: number; + dateRange: { + start: Date; + end: Date; + }; + overallStats: { + totalTransfers: number; + avgSuccessRate: number; + avgSettlementTimeMs: number; + avgFee: number; + avgSlippagePercent: number; + totalVolume: number; + }; + bestPeriod?: { + timestamp: Date; + successRate: number; + }; + worstPeriod?: { + timestamp: Date; + successRate: number; + }; +}