This commit is contained in:
Boki 2025-06-22 17:55:51 -04:00
parent d858222af7
commit 7d9044ab29
202 changed files with 10755 additions and 10972 deletions

View file

@ -37,25 +37,25 @@ export type {
HasClose,
HasOHLC,
HasVolume,
HasTimestamp
HasTimestamp,
} from '@stock-bot/types';
// Export working calculation functions
export * from './basic-calculations';
// Export working technical indicators (building one by one)
export {
sma,
ema,
rsi,
macd,
bollingerBands,
atr,
obv,
stochastic,
williamsR,
cci,
mfi,
export {
sma,
ema,
rsi,
macd,
bollingerBands,
atr,
obv,
stochastic,
williamsR,
cci,
mfi,
vwma,
momentum,
roc,
@ -80,7 +80,7 @@ export {
balanceOfPower,
trix,
massIndex,
coppockCurve
coppockCurve,
} from './technical-indicators';
export * from './risk-metrics';
export * from './performance-metrics';

View file

@ -1,3 +1,5 @@
import { ulcerIndex } from './risk-metrics';
/**
* Performance Metrics and Analysis
* Comprehensive performance measurement tools for trading strategies and portfolios
@ -18,7 +20,6 @@ export interface PortfolioMetrics {
alpha: number;
volatility: number;
}
import { ulcerIndex } from './risk-metrics';
export interface TradePerformance {
totalTrades: number;
@ -156,8 +157,10 @@ export function analyzeDrawdowns(
}
const first = equityCurve[0];
if (!first) {return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] };}
if (!first) {
return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] };
}
let peak = first.value;
let peakDate = first.date;
let maxDrawdown = 0;
@ -175,18 +178,21 @@ export function analyzeDrawdowns(
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
if (!current) {continue;}
if (!current) {
continue;
}
if (current.value > peak) {
// New peak - end any current drawdown
if (currentDrawdownStart) {
const prev = equityCurve[i - 1];
if (!prev) {continue;}
if (!prev) {
continue;
}
const drawdownMagnitude = (peak - prev.value) / peak;
const duration = Math.floor(
(prev.date.getTime() - currentDrawdownStart.getTime()) /
(1000 * 60 * 60 * 24)
(prev.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)
);
drawdownPeriods.push({
@ -217,8 +223,10 @@ export function analyzeDrawdowns(
// Handle ongoing drawdown
if (currentDrawdownStart) {
const lastPoint = equityCurve[equityCurve.length - 1];
if (!lastPoint) {return { maxDrawdown, maxDrawdownDuration, averageDrawdown: 0, drawdownPeriods };}
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)
@ -378,8 +386,10 @@ export function strategyPerformanceAttribution(
for (let i = 0; i < sectorWeights.length; i++) {
const portfolioWeight = sectorWeights[i];
const sectorReturn = sectorReturns[i];
if (portfolioWeight === undefined || sectorReturn === undefined) {continue;}
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)
@ -483,16 +493,31 @@ export function calculateStrategyMetrics(
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
const previous = equityCurve[i - 1];
if (!current || !previous) {continue;}
if (!current || !previous) {
continue;
}
const ret = (current.value - previous.value) / previous.value;
returns.push(ret);
}
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 };}
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;
@ -562,12 +587,10 @@ export function informationRatio(portfolioReturns: number[], benchmarkReturns: n
throw new Error('Portfolio and benchmark returns must have the same length.');
}
const excessReturns = portfolioReturns.map(
(portfolioReturn, index) => {
const benchmark = benchmarkReturns[index];
return benchmark !== undefined ? portfolioReturn - benchmark : 0;
}
);
const excessReturns = portfolioReturns.map((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;
@ -602,8 +625,10 @@ export function captureRatio(
for (let i = 0; i < portfolioReturns.length; i++) {
const benchmarkReturn = benchmarkReturns[i];
const portfolioReturn = portfolioReturns[i];
if (benchmarkReturn === undefined || portfolioReturn === undefined) {continue;}
if (benchmarkReturn === undefined || portfolioReturn === undefined) {
continue;
}
if (benchmarkReturn > 0) {
upCapture += portfolioReturn;
upMarketPeriods++;
@ -733,17 +758,21 @@ export function timeWeightedRateOfReturn(
if (cashFlows.length < 2) {
return 0;
}
const first = cashFlows[0];
if (!first) {return 0;}
if (!first) {
return 0;
}
let totalReturn = 1;
let previousValue = first.value;
for (let i = 1; i < cashFlows.length; i++) {
const current = cashFlows[i];
if (!current) {continue;}
if (!current) {
continue;
}
const periodReturn =
(current.value - previousValue - current.amount) / (previousValue + current.amount);
totalReturn *= 1 + periodReturn;
@ -762,10 +791,12 @@ export function moneyWeightedRateOfReturn(
if (cashFlows.length === 0) {
return 0;
}
const first = cashFlows[0];
if (!first) {return 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
@ -826,8 +857,10 @@ function calculateBeta(portfolioReturns: number[], marketReturns: number[]): num
for (let i = 0; i < portfolioReturns.length; i++) {
const portfolioReturn = portfolioReturns[i];
const marketReturn = marketReturns[i];
if (portfolioReturn === undefined || marketReturn === undefined) {continue;}
if (portfolioReturn === undefined || marketReturn === undefined) {
continue;
}
const portfolioDiff = portfolioReturn - portfolioMean;
const marketDiff = marketReturn - marketMean;

View file

@ -71,14 +71,18 @@ export function maxDrawdown(equityCurve: number[]): number {
let maxDD = 0;
const first = equityCurve[0];
if (first === undefined) {return 0;}
if (first === undefined) {
return 0;
}
let peak = first;
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
if (current === undefined) {continue;}
if (current === undefined) {
continue;
}
if (current > peak) {
peak = current;
} else {
@ -150,8 +154,10 @@ export function beta(portfolioReturns: number[], marketReturns: number[]): numbe
for (let i = 0; i < n; i++) {
const portfolioReturn = portfolioReturns[i];
const marketReturn = marketReturns[i];
if (portfolioReturn === undefined || marketReturn === undefined) {continue;}
if (portfolioReturn === undefined || marketReturn === undefined) {
continue;
}
const portfolioDiff = portfolioReturn - portfolioMean;
const marketDiff = marketReturn - marketMean;
@ -187,12 +193,13 @@ export function treynorRatio(
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;
const portfolioMean =
portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
return (portfolioMean - riskFreeRate) / portfolioBeta;
}
@ -412,7 +419,9 @@ export function riskContribution(
for (let i = 0; i < n; i++) {
let marginalContribution = 0;
const row = covarianceMatrix[i];
if (!row) {continue;}
if (!row) {
continue;
}
for (let j = 0; j < n; j++) {
const weight = weights[j];
@ -442,8 +451,10 @@ export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): n
let sumSquaredDrawdown = 0;
const first = equityCurve[0];
if (!first) {return 0;}
if (!first) {
return 0;
}
let peak = first.value;
for (const point of equityCurve) {

View file

@ -540,7 +540,9 @@ 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,8 +577,10 @@ 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;
@ -602,17 +606,15 @@ export function adx(
/**
* Parabolic SAR
*/
export function parabolicSAR(
ohlcv: OHLCV[],
step: number = 0.02,
maxStep: number = 0.2
): number[] {
export function parabolicSAR(ohlcv: OHLCV[], step: number = 0.02, maxStep: number = 0.2): number[] {
if (ohlcv.length < 2) {
return [];
}
const first = ohlcv[0];
if (!first) {return [];}
if (!first) {
return [];
}
const result: number[] = [];
let trend = 1; // 1 for uptrend, -1 for downtrend
@ -625,7 +627,9 @@ 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);
@ -834,32 +838,37 @@ export function ultimateOscillator(
// Calculate BP and TR
for (let i = 0; i < ohlcv.length; i++) {
const current = ohlcv[i]!;
if (i === 0) {
bp.push(current.close - current.low);
tr.push(current.high - current.low);
} else {
const previous = ohlcv[i - 1]!;
bp.push(current.close - Math.min(current.low, previous.close));
tr.push(Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
));
tr.push(
Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
)
);
}
}
const result: number[] = [];
for (let i = Math.max(period1, period2, period3) - 1; i < ohlcv.length; i++) {
const avg1 = bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg2 = bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg3 = bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg1 =
bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg2 =
bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg3 =
bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0);
const uo = 100 * ((4 * avg1) + (2 * avg2) + avg3) / (4 + 2 + 1);
const uo = (100 * (4 * avg1 + 2 * avg2 + avg3)) / (4 + 2 + 1);
result.push(uo);
}
@ -880,7 +889,7 @@ export function easeOfMovement(ohlcv: OHLCV[], period: number = 14): number[] {
const current = ohlcv[i]!;
const previous = ohlcv[i - 1]!;
const distance = ((current.high + current.low) / 2) - ((previous.high + previous.low) / 2);
const distance = (current.high + current.low) / 2 - (previous.high + previous.low) / 2;
const boxHeight = current.high - current.low;
const volume = current.volume;
@ -1028,7 +1037,14 @@ export function klingerVolumeOscillator(
const prevTypicalPrice = (previous.high + previous.low + previous.close) / 3;
const trend = typicalPrice > prevTypicalPrice ? 1 : -1;
const vf = current.volume * trend * Math.abs((2 * ((current.close - current.low) - (current.high - current.close))) / (current.high - current.low)) * 100;
const vf =
current.volume *
trend *
Math.abs(
(2 * (current.close - current.low - (current.high - current.close))) /
(current.high - current.low)
) *
100;
volumeForce.push(vf);
}
@ -1137,7 +1153,7 @@ export function stochasticRSI(
smoothD: number = 3
): { k: number[]; d: number[] } {
const rsiValues = rsi(prices, rsiPeriod);
if (rsiValues.length < stochPeriod) {
return { k: [], d: [] };
}
@ -1266,17 +1282,17 @@ export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] {
// Calculate high-low ranges
const ranges = ohlcv.map(candle => candle.high - candle.low);
// Calculate 9-period EMA of ranges
const ema9 = ema(ranges, 9);
// Calculate 9-period EMA of the EMA (double smoothing)
const emaEma9 = ema(ema9, 9);
// Calculate ratio
const ratios: number[] = [];
const minLength = Math.min(ema9.length, emaEma9.length);
for (let i = 0; i < minLength; i++) {
const singleEMA = ema9[i];
const doubleEMA = emaEma9[i];
@ -1299,9 +1315,9 @@ export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] {
* Coppock Curve
*/
export function coppockCurve(
prices: number[],
shortROC: number = 11,
longROC: number = 14,
prices: number[],
shortROC: number = 11,
longROC: number = 14,
wma: number = 10
): number[] {
const roc1 = roc(prices, shortROC);