fixed a lot of lint and work on utils
This commit is contained in:
parent
4881a38c32
commit
42bc2966df
17 changed files with 183 additions and 93 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue