From 42bc2966df33e16b0cced8f3803b8987b4dc3771 Mon Sep 17 00:00:00 2001 From: Boki Date: Thu, 19 Jun 2025 21:07:37 -0400 Subject: [PATCH] fixed a lot of lint and work on utils --- .../src/services/sync-manager.ts | 2 +- apps/web-app/src/components/layout/Layout.tsx | 2 +- apps/web-app/src/components/ui/dialog.tsx | 3 +- .../dashboard/components/PortfolioTable.tsx | 14 +- .../components/AddProviderMappingDialog.tsx | 2 +- .../exchanges/components/AddSourceDialog.tsx | 6 +- .../exchanges/hooks/useFormValidation.ts | 2 +- apps/web-app/src/lib/utils.ts | 4 +- apps/web-app/src/lib/utils/index.ts | 12 +- libs/config/src/loaders/env.loader.ts | 10 +- libs/queue/src/rate-limiter.ts | 6 +- libs/queue/test/rate-limiter.test.ts | 4 +- libs/utils/src/calculations/index.ts | 4 +- ...ics.ts.disabled => performance-metrics.ts} | 120 ++++++++++++------ ...sk-metrics.ts.disabled => risk-metrics.ts} | 73 +++++++++-- .../src/calculations/technical-indicators.ts | 8 +- .../test/calculations/position-sizing.test.ts | 4 - 17 files changed, 183 insertions(+), 93 deletions(-) rename libs/utils/src/calculations/{performance-metrics.ts.disabled => performance-metrics.ts} (88%) rename libs/utils/src/calculations/{risk-metrics.ts.disabled => risk-metrics.ts} (85%) diff --git a/apps/data-sync-service/src/services/sync-manager.ts b/apps/data-sync-service/src/services/sync-manager.ts index 7336321..8d0fe64 100644 --- a/apps/data-sync-service/src/services/sync-manager.ts +++ b/apps/data-sync-service/src/services/sync-manager.ts @@ -162,7 +162,7 @@ export class SyncManager { // Helper methods private async resolveExchange(exchangeCode: string): Promise { - if (!exchangeCode) return null; + if (!exchangeCode) {return null;} // Simple mapping - expand this as needed const exchangeMap: Record = { diff --git a/apps/web-app/src/components/layout/Layout.tsx b/apps/web-app/src/components/layout/Layout.tsx index 24b711f..6f50405 100644 --- a/apps/web-app/src/components/layout/Layout.tsx +++ b/apps/web-app/src/components/layout/Layout.tsx @@ -10,7 +10,7 @@ export function Layout() { // Determine title from current route const getTitle = () => { const path = location.pathname.replace('/', ''); - if (!path || path === 'dashboard') return 'Dashboard'; + if (!path || path === 'dashboard') {return 'Dashboard';} return path.charAt(0).toUpperCase() + path.slice(1); }; diff --git a/apps/web-app/src/components/ui/dialog.tsx b/apps/web-app/src/components/ui/dialog.tsx index ea342a2..f671beb 100644 --- a/apps/web-app/src/components/ui/dialog.tsx +++ b/apps/web-app/src/components/ui/dialog.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { XMarkIcon } from '@heroicons/react/24/outline'; interface DialogProps { open: boolean; @@ -8,7 +7,7 @@ interface DialogProps { } export function Dialog({ open, onOpenChange, children }: DialogProps) { - if (!open) return null; + if (!open) {return null;} return (
diff --git a/apps/web-app/src/features/dashboard/components/PortfolioTable.tsx b/apps/web-app/src/features/dashboard/components/PortfolioTable.tsx index 39023c3..ccf7853 100644 --- a/apps/web-app/src/features/dashboard/components/PortfolioTable.tsx +++ b/apps/web-app/src/features/dashboard/components/PortfolioTable.tsx @@ -278,9 +278,9 @@ export function PortfolioTable() { size: 120, cell: ({ getValue }) => { const value = getValue() as number; - if (value >= 1e12) return ${(value / 1e12).toFixed(2)}T; - if (value >= 1e9) return ${(value / 1e9).toFixed(2)}B; - if (value >= 1e6) return ${(value / 1e6).toFixed(2)}M; + if (value >= 1e12) {return ${(value / 1e12).toFixed(2)}T;} + if (value >= 1e9) {return ${(value / 1e9).toFixed(2)}B;} + if (value >= 1e6) {return ${(value / 1e6).toFixed(2)}M;} return ${value.toLocaleString()}; }, }, @@ -327,8 +327,8 @@ export function PortfolioTable() { size: 120, cell: ({ getValue }) => { const value = getValue() as number; - if (value >= 1e9) return ${(value / 1e9).toFixed(2)}B; - if (value >= 1e6) return ${(value / 1e6).toFixed(2)}M; + if (value >= 1e9) {return ${(value / 1e9).toFixed(2)}B;} + if (value >= 1e6) {return ${(value / 1e6).toFixed(2)}M;} return ${value.toLocaleString()}; }, }, @@ -339,8 +339,8 @@ export function PortfolioTable() { size: 120, cell: ({ getValue }) => { const value = getValue() as number; - if (value >= 1e9) return ${(value / 1e9).toFixed(2)}B; - if (value >= 1e6) return ${(value / 1e6).toFixed(2)}M; + if (value >= 1e9) {return ${(value / 1e9).toFixed(2)}B;} + if (value >= 1e6) {return ${(value / 1e6).toFixed(2)}M;} return ${value.toLocaleString()}; }, }, diff --git a/apps/web-app/src/features/exchanges/components/AddProviderMappingDialog.tsx b/apps/web-app/src/features/exchanges/components/AddProviderMappingDialog.tsx index 9fcbfd2..85720fe 100644 --- a/apps/web-app/src/features/exchanges/components/AddProviderMappingDialog.tsx +++ b/apps/web-app/src/features/exchanges/components/AddProviderMappingDialog.tsx @@ -200,7 +200,7 @@ export function AddProviderMappingDialog({ const exchange = unmappedExchanges.find( ex => ex.provider_exchange_code === selectedProviderExchange ); - if (!exchange) return null; + if (!exchange) {return null;} return (
diff --git a/apps/web-app/src/features/exchanges/components/AddSourceDialog.tsx b/apps/web-app/src/features/exchanges/components/AddSourceDialog.tsx index 7bc7813..4a5b0cd 100644 --- a/apps/web-app/src/features/exchanges/components/AddSourceDialog.tsx +++ b/apps/web-app/src/features/exchanges/components/AddSourceDialog.tsx @@ -27,7 +27,7 @@ export function AddSourceDialog({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!source || !sourceCode || !id || !name || !code) return; + if (!source || !sourceCode || !id || !name || !code) {return;} setLoading(true); try { @@ -52,8 +52,8 @@ export function AddSourceDialog({ setName(''); setCode(''); setAliases(''); - } catch (error) { - console.error('Error adding source:', error); + } catch (_error) { + console.error('Error adding source:', _error); } finally { setLoading(false); } diff --git a/apps/web-app/src/features/exchanges/hooks/useFormValidation.ts b/apps/web-app/src/features/exchanges/hooks/useFormValidation.ts index db53f80..41f4bf1 100644 --- a/apps/web-app/src/features/exchanges/hooks/useFormValidation.ts +++ b/apps/web-app/src/features/exchanges/hooks/useFormValidation.ts @@ -35,7 +35,7 @@ export function useFormValidation( onSuccess?: () => void, onError?: (error: unknown) => void ) => { - if (!validate()) return; + if (!validate()) {return;} setIsSubmitting(true); try { diff --git a/apps/web-app/src/lib/utils.ts b/apps/web-app/src/lib/utils.ts index 6b52e6e..be7fef6 100644 --- a/apps/web-app/src/lib/utils.ts +++ b/apps/web-app/src/lib/utils.ts @@ -19,7 +19,7 @@ export function formatPercentage(value: number): string { } export function getValueColor(value: number): string { - if (value > 0) return 'text-success'; - if (value < 0) return 'text-danger'; + if (value > 0) {return 'text-success';} + if (value < 0) {return 'text-danger';} return 'text-text-secondary'; } diff --git a/apps/web-app/src/lib/utils/index.ts b/apps/web-app/src/lib/utils/index.ts index 520cc66..e365e72 100644 --- a/apps/web-app/src/lib/utils/index.ts +++ b/apps/web-app/src/lib/utils/index.ts @@ -23,9 +23,9 @@ export function formatPercentage(value: number, decimals = 2): string { * Format large numbers with K, M, B suffixes */ export function formatNumber(num: number): string { - if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B'; - if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M'; - if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K'; + if (num >= 1e9) {return (num / 1e9).toFixed(1) + 'B';} + if (num >= 1e6) {return (num / 1e6).toFixed(1) + 'M';} + if (num >= 1e3) {return (num / 1e3).toFixed(1) + 'K';} return num.toString(); } @@ -33,8 +33,8 @@ export function formatNumber(num: number): string { * Get color class based on numeric value (profit/loss) */ export function getValueColor(value: number): string { - if (value > 0) return 'text-success'; - if (value < 0) return 'text-danger'; + if (value > 0) {return 'text-success';} + if (value < 0) {return 'text-danger';} return 'text-text-secondary'; } @@ -42,6 +42,6 @@ export function getValueColor(value: number): string { * Truncate text to specified length */ export function truncateText(text: string, length: number): string { - if (text.length <= length) return text; + if (text.length <= length) {return text;} return text.slice(0, length) + '...'; } diff --git a/libs/config/src/loaders/env.loader.ts b/libs/config/src/loaders/env.loader.ts index 68b241e..e5b421e 100644 --- a/libs/config/src/loaders/env.loader.ts +++ b/libs/config/src/loaders/env.loader.ts @@ -110,16 +110,16 @@ export class EnvLoader implements ConfigLoader { } // Handle booleans - if (value.toLowerCase() === 'true') return true; - if (value.toLowerCase() === 'false') return false; + if (value.toLowerCase() === 'true') {return true;} + if (value.toLowerCase() === 'false') {return false;} // Handle numbers const num = Number(value); - if (!isNaN(num) && value !== '') return num; + if (!isNaN(num) && value !== '') {return num;} // Handle null/undefined - if (value.toLowerCase() === 'null') return null; - if (value.toLowerCase() === 'undefined') return undefined; + if (value.toLowerCase() === 'null') {return null;} + if (value.toLowerCase() === 'undefined') {return undefined;} // Return as string return value; diff --git a/libs/queue/src/rate-limiter.ts b/libs/queue/src/rate-limiter.ts index 2293a1c..1ef7042 100644 --- a/libs/queue/src/rate-limiter.ts +++ b/libs/queue/src/rate-limiter.ts @@ -93,7 +93,7 @@ export class QueueRateLimiter { r.handler === handler && r.operation === operation ); - if (rule) return rule; + if (rule) {return rule;} // 2. Check for handler-specific rule rule = this.rules.find(r => @@ -101,14 +101,14 @@ export class QueueRateLimiter { r.queueName === queueName && r.handler === handler ); - if (rule) return rule; + if (rule) {return rule;} // 3. Check for queue-specific rule rule = this.rules.find(r => r.level === 'queue' && r.queueName === queueName ); - if (rule) return rule; + if (rule) {return rule;} // 4. Check for global rule (least specific) rule = this.rules.find(r => r.level === 'global'); diff --git a/libs/queue/test/rate-limiter.test.ts b/libs/queue/test/rate-limiter.test.ts index 09bd122..4906705 100644 --- a/libs/queue/test/rate-limiter.test.ts +++ b/libs/queue/test/rate-limiter.test.ts @@ -210,7 +210,7 @@ describe('QueueRateLimiter', () => { // Consume the limit await rateLimiter.checkLimit('reset-test', 'operation'); - let blocked = await rateLimiter.checkLimit('reset-test', 'operation'); + const blocked = await rateLimiter.checkLimit('reset-test', 'operation'); expect(blocked.allowed).toBe(false); // Reset limits @@ -248,7 +248,7 @@ describe('QueueRateLimiter', () => { // Verify rule exists await rateLimiter.checkLimit('remove-test', 'op'); - let blocked = await rateLimiter.checkLimit('remove-test', 'op'); + const blocked = await rateLimiter.checkLimit('remove-test', 'op'); expect(blocked.allowed).toBe(false); // Remove rule diff --git a/libs/utils/src/calculations/index.ts b/libs/utils/src/calculations/index.ts index 97f0d30..e24dd5f 100644 --- a/libs/utils/src/calculations/index.ts +++ b/libs/utils/src/calculations/index.ts @@ -82,11 +82,11 @@ export { massIndex, coppockCurve } from './technical-indicators'; -// export * from './risk-metrics'; +export * from './risk-metrics'; // export * from './portfolio-analytics'; // export * from './options-pricing'; // export * from './position-sizing'; -// export * from './performance-metrics'; +export * from './performance-metrics'; // export * from './market-statistics'; // export * from './volatility-models'; // export * from './correlation-analysis'; diff --git a/libs/utils/src/calculations/performance-metrics.ts.disabled b/libs/utils/src/calculations/performance-metrics.ts similarity index 88% rename from libs/utils/src/calculations/performance-metrics.ts.disabled rename to libs/utils/src/calculations/performance-metrics.ts index 72a8f7f..8a2aa28 100644 --- a/libs/utils/src/calculations/performance-metrics.ts.disabled +++ b/libs/utils/src/calculations/performance-metrics.ts @@ -3,7 +3,22 @@ * Comprehensive performance measurement tools for trading strategies and portfolios */ -import { PortfolioMetrics, ulcerIndex } from './index'; +// import type { PortfolioMetrics } from '@stock-bot/types'; + +// Define PortfolioMetrics locally until it's added to types library +export interface PortfolioMetrics { + totalValue: number; + totalReturn: number; + totalReturnPercent: number; + dailyReturn: number; + dailyReturnPercent: number; + maxDrawdown: number; + sharpeRatio: number; + beta: number; + alpha: number; + volatility: number; +} +import { ulcerIndex } from './risk-metrics'; export interface TradePerformance { totalTrades: number; @@ -140,8 +155,11 @@ export function analyzeDrawdowns( }; } - let peak = equityCurve[0].value; - let peakDate = equityCurve[0].date; + const first = equityCurve[0]; + if (!first) {return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] };} + + let peak = first.value; + let peakDate = first.date; let maxDrawdown = 0; let maxDrawdownDuration = 0; @@ -157,19 +175,23 @@ export function analyzeDrawdowns( for (let i = 1; i < equityCurve.length; i++) { const current = equityCurve[i]; + if (!current) {continue;} if (current.value > peak) { // New peak - end any current drawdown if (currentDrawdownStart) { - const drawdownMagnitude = (peak - equityCurve[i - 1].value) / peak; + const prev = equityCurve[i - 1]; + if (!prev) {continue;} + + const drawdownMagnitude = (peak - prev.value) / peak; const duration = Math.floor( - (equityCurve[i - 1].date.getTime() - currentDrawdownStart.getTime()) / + (prev.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24) ); drawdownPeriods.push({ start: currentDrawdownStart, - end: equityCurve[i - 1].date, + end: prev.date, duration, magnitude: drawdownMagnitude, }); @@ -195,6 +217,8 @@ export function analyzeDrawdowns( // Handle ongoing drawdown if (currentDrawdownStart) { const lastPoint = equityCurve[equityCurve.length - 1]; + if (!lastPoint) {return { maxDrawdown, maxDrawdownDuration, averageDrawdown: 0, drawdownPeriods };} + const drawdownMagnitude = (peak - lastPoint.value) / peak; const duration = Math.floor( (lastPoint.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24) @@ -352,8 +376,10 @@ export function strategyPerformanceAttribution( for (let i = 0; i < sectorWeights.length; i++) { const portfolioWeight = sectorWeights[i]; - const benchmarkWeight = 1 / sectorWeights.length; // Assuming equal benchmark weights const sectorReturn = sectorReturns[i]; + if (portfolioWeight === undefined || sectorReturn === undefined) {continue;} + + const benchmarkWeight = 1 / sectorWeights.length; // Assuming equal benchmark weights // Allocation effect: (portfolio weight - benchmark weight) * (benchmark sector return - benchmark return) allocationEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - benchmarkReturn); @@ -454,15 +480,23 @@ export function calculateStrategyMetrics( const returns = []; for (let i = 1; i < equityCurve.length; i++) { - const ret = (equityCurve[i].value - equityCurve[i - 1].value) / equityCurve[i - 1].value; + const current = equityCurve[i]; + const previous = equityCurve[i - 1]; + if (!current || !previous) {continue;} + + const ret = (current.value - previous.value) / previous.value; returns.push(ret); } - const totalValue = equityCurve[equityCurve.length - 1].value; - const totalReturn = totalValue - equityCurve[0].value; - const totalReturnPercent = (totalReturn / equityCurve[0].value) * 100; + const lastPoint = equityCurve[equityCurve.length - 1]; + const firstPoint = equityCurve[0]; + if (!lastPoint || !firstPoint) {return { totalValue: 0, totalReturn: 0, totalReturnPercent: 0, dailyReturn: 0, dailyReturnPercent: 0, maxDrawdown: 0, sharpeRatio: 0, beta: 0, alpha: 0, volatility: 0 };} + + const totalValue = lastPoint.value; + const totalReturn = totalValue - firstPoint.value; + const totalReturnPercent = (totalReturn / firstPoint.value) * 100; - const dailyReturn = returns[returns.length - 1]; + const dailyReturn = returns[returns.length - 1] || 0; const dailyReturnPercent = dailyReturn * 100; const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown; @@ -528,7 +562,10 @@ export function informationRatio(portfolioReturns: number[], benchmarkReturns: n } const excessReturns = portfolioReturns.map( - (portfolioReturn, index) => portfolioReturn - benchmarkReturns[index] + (portfolioReturn, index) => { + const benchmark = benchmarkReturns[index]; + return benchmark !== undefined ? portfolioReturn - benchmark : 0; + } ); const trackingError = calculateVolatility(excessReturns); const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length; @@ -536,20 +573,7 @@ export function informationRatio(portfolioReturns: number[], benchmarkReturns: n return trackingError === 0 ? 0 : avgExcessReturn / trackingError; } -/** - * Calculate Treynor Ratio - */ -export function treynorRatio( - portfolioReturns: number[], - marketReturns: number[], - riskFreeRate: number -): number { - const beta = calculateBeta(portfolioReturns, marketReturns); - const avgPortfolioReturn = - portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length; - - return beta === 0 ? 0 : (avgPortfolioReturn - riskFreeRate) / beta; -} +// Treynor Ratio is already exported from risk-metrics /** * Calculate Jensen's Alpha (same as Alpha, but included for clarity) @@ -575,11 +599,15 @@ export function captureRatio( let downMarketPeriods = 0; for (let i = 0; i < portfolioReturns.length; i++) { - if (benchmarkReturns[i] > 0) { - upCapture += portfolioReturns[i]; + const benchmarkReturn = benchmarkReturns[i]; + const portfolioReturn = portfolioReturns[i]; + if (benchmarkReturn === undefined || portfolioReturn === undefined) {continue;} + + if (benchmarkReturn > 0) { + upCapture += portfolioReturn; upMarketPeriods++; - } else if (benchmarkReturns[i] < 0) { - downCapture += portfolioReturns[i]; + } else if (benchmarkReturn < 0) { + downCapture += portfolioReturn; downMarketPeriods++; } } @@ -701,11 +729,20 @@ export function calculateRollingAlpha( export function timeWeightedRateOfReturn( cashFlows: Array<{ amount: number; date: Date; value: number }> ): number { + if (cashFlows.length < 2) { + return 0; + } + + const first = cashFlows[0]; + if (!first) {return 0;} + let totalReturn = 1; - let previousValue = cashFlows[0].value; + let previousValue = first.value; for (let i = 1; i < cashFlows.length; i++) { const current = cashFlows[i]; + if (!current) {continue;} + const periodReturn = (current.value - previousValue - current.amount) / (previousValue + current.amount); totalReturn *= 1 + periodReturn; @@ -721,13 +758,20 @@ export function timeWeightedRateOfReturn( export function moneyWeightedRateOfReturn( cashFlows: Array<{ amount: number; date: Date; value: number }> ): number { + if (cashFlows.length === 0) { + return 0; + } + + const first = cashFlows[0]; + if (!first) {return 0;} + // Approximate MWRR using Internal Rate of Return (IRR) // This requires a numerical method or library for accurate IRR calculation // This is a simplified example and may not be accurate for all cases let totalCashFlow = 0; let totalWeightedCashFlow = 0; - const startDate = cashFlows[0].date.getTime(); + const startDate = first.date.getTime(); for (const cf of cashFlows) { const timeDiff = (cf.date.getTime() - startDate) / (1000 * 60 * 60 * 24 * 365); // Years @@ -736,7 +780,7 @@ export function moneyWeightedRateOfReturn( } // Simplified approximation: MWRR ≈ totalCashFlow / totalWeightedCashFlow - 1 - return totalCashFlow / totalWeightedCashFlow - 1; + return totalWeightedCashFlow === 0 ? 0 : totalCashFlow / totalWeightedCashFlow - 1; } // Helper functions @@ -779,8 +823,12 @@ function calculateBeta(portfolioReturns: number[], marketReturns: number[]): num let marketVariance = 0; for (let i = 0; i < portfolioReturns.length; i++) { - const portfolioDiff = portfolioReturns[i] - portfolioMean; - const marketDiff = marketReturns[i] - marketMean; + const portfolioReturn = portfolioReturns[i]; + const marketReturn = marketReturns[i]; + if (portfolioReturn === undefined || marketReturn === undefined) {continue;} + + const portfolioDiff = portfolioReturn - portfolioMean; + const marketDiff = marketReturn - marketMean; covariance += portfolioDiff * marketDiff; marketVariance += marketDiff * marketDiff; diff --git a/libs/utils/src/calculations/risk-metrics.ts.disabled b/libs/utils/src/calculations/risk-metrics.ts similarity index 85% rename from libs/utils/src/calculations/risk-metrics.ts.disabled rename to libs/utils/src/calculations/risk-metrics.ts index ffb2343..80cd20d 100644 --- a/libs/utils/src/calculations/risk-metrics.ts.disabled +++ b/libs/utils/src/calculations/risk-metrics.ts @@ -3,7 +3,7 @@ * Comprehensive risk measurement tools for portfolio and trading analysis */ -import { RiskMetrics, treynorRatio } from './index'; +import type { RiskMetrics } from '@stock-bot/types'; /** * Calculate Value at Risk (VaR) using historical simulation @@ -31,7 +31,7 @@ export function conditionalValueAtRisk(returns: number[], confidenceLevel: numbe const cutoffIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length); if (cutoffIndex === 0) { - return sortedReturns[0]; + return sortedReturns[0] || 0; } const tailReturns = sortedReturns.slice(0, cutoffIndex); @@ -70,13 +70,19 @@ export function maxDrawdown(equityCurve: number[]): number { } let maxDD = 0; - let peak = equityCurve[0]; + const first = equityCurve[0]; + if (first === undefined) {return 0;} + + let peak = first; for (let i = 1; i < equityCurve.length; i++) { - if (equityCurve[i] > peak) { - peak = equityCurve[i]; + const current = equityCurve[i]; + if (current === undefined) {continue;} + + if (current > peak) { + peak = current; } else { - const drawdown = (peak - equityCurve[i]) / peak; + const drawdown = (peak - current) / peak; maxDD = Math.max(maxDD, drawdown); } } @@ -142,8 +148,12 @@ export function beta(portfolioReturns: number[], marketReturns: number[]): numbe let marketVariance = 0; for (let i = 0; i < n; i++) { - const portfolioDiff = portfolioReturns[i] - portfolioMean; - const marketDiff = marketReturns[i] - marketMean; + const portfolioReturn = portfolioReturns[i]; + const marketReturn = marketReturns[i]; + if (portfolioReturn === undefined || marketReturn === undefined) {continue;} + + const portfolioDiff = portfolioReturn - portfolioMean; + const marketDiff = marketReturn - marketMean; covariance += portfolioDiff * marketDiff; marketVariance += marketDiff * marketDiff; @@ -168,6 +178,24 @@ export function alpha( return portfolioMean - (riskFreeRate + portfolioBeta * (marketMean - riskFreeRate)); } +/** + * Calculate Treynor ratio + */ +export function treynorRatio( + portfolioReturns: number[], + marketReturns: number[], + riskFreeRate: number = 0 +): number { + const portfolioBeta = beta(portfolioReturns, marketReturns); + + if (portfolioBeta === 0) { + return 0; + } + + const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length; + return (portfolioMean - riskFreeRate) / portfolioBeta; +} + /** * Calculate tracking error */ @@ -176,7 +204,10 @@ export function trackingError(portfolioReturns: number[], benchmarkReturns: numb return 0; } - const activeReturns = portfolioReturns.map((ret, i) => ret - benchmarkReturns[i]); + const activeReturns = portfolioReturns.map((ret, i) => { + const benchmark = benchmarkReturns[i]; + return benchmark !== undefined ? ret - benchmark : 0; + }); const mean = activeReturns.reduce((sum, ret) => sum + ret, 0) / activeReturns.length; const variance = @@ -380,13 +411,22 @@ export function riskContribution( for (let i = 0; i < n; i++) { let marginalContribution = 0; + const row = covarianceMatrix[i]; + if (!row) {continue;} for (let j = 0; j < n; j++) { - marginalContribution += weights[j] * covarianceMatrix[i][j]; + const weight = weights[j]; + const covariance = row[j]; + if (weight !== undefined && covariance !== undefined) { + marginalContribution += weight * covariance; + } } - const contribution = (weights[i] * marginalContribution) / Math.pow(portfolioVolatility, 2); - contributions.push(contribution); + const weight = weights[i]; + if (weight !== undefined && portfolioVolatility !== 0) { + const contribution = (weight * marginalContribution) / Math.pow(portfolioVolatility, 2); + contributions.push(contribution); + } } return contributions; @@ -396,8 +436,15 @@ export function riskContribution( * Calculate Ulcer Index */ export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number { + if (equityCurve.length === 0) { + return 0; + } + let sumSquaredDrawdown = 0; - let peak = equityCurve[0].value; + const first = equityCurve[0]; + if (!first) {return 0;} + + let peak = first.value; for (const point of equityCurve) { peak = Math.max(peak, point.value); diff --git a/libs/utils/src/calculations/technical-indicators.ts b/libs/utils/src/calculations/technical-indicators.ts index 715cc40..d45fec9 100644 --- a/libs/utils/src/calculations/technical-indicators.ts +++ b/libs/utils/src/calculations/technical-indicators.ts @@ -540,7 +540,7 @@ export function adx( for (let i = 1; i < ohlcv.length; i++) { const current = ohlcv[i]; const previous = ohlcv[i - 1]; - if (!current || !previous) continue; + if (!current || !previous) {continue;} // True Range const tr = Math.max( @@ -575,7 +575,7 @@ export function adx( const atr = atrValues[i]; const plusDMSmoothed = smoothedPlusDM[i]; const minusDMSmoothed = smoothedMinusDM[i]; - if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) continue; + if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) {continue;} const diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0; const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0; @@ -612,7 +612,7 @@ export function parabolicSAR( } const first = ohlcv[0]; - if (!first) return []; + if (!first) {return [];} const result: number[] = []; let trend = 1; // 1 for uptrend, -1 for downtrend @@ -625,7 +625,7 @@ export function parabolicSAR( for (let i = 1; i < ohlcv.length; i++) { const curr = ohlcv[i]; const prev = ohlcv[i - 1]; - if (!curr || !prev) continue; + if (!curr || !prev) {continue;} // Calculate new SAR sar = sar + acceleration * (extremePoint - sar); diff --git a/libs/utils/test/calculations/position-sizing.test.ts b/libs/utils/test/calculations/position-sizing.test.ts index ff776d6..2c2e999 100644 --- a/libs/utils/test/calculations/position-sizing.test.ts +++ b/libs/utils/test/calculations/position-sizing.test.ts @@ -9,17 +9,13 @@ import { dynamicPositionSize, equalWeightPositionSize, expectancyPositionSize, - fixedFractionalPositionSize, fixedRiskPositionSize, - fractionalKellyPositionSize, kellyPositionSize, liquidityConstrainedPositionSize, - monteCarloPositionSize, multiTimeframePositionSize, riskParityPositionSize, sharpeOptimizedPositionSize, validatePositionSize, - volatilityAdjustedPositionSize, volatilityTargetPositionSize, type KellyParams, type PositionSizeParams,