fixed a lot of lint and work on utils

This commit is contained in:
Boki 2025-06-19 21:07:37 -04:00
parent 4881a38c32
commit 42bc2966df
17 changed files with 183 additions and 93 deletions

View file

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

View file

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

View file

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

View file

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