format
This commit is contained in:
parent
d858222af7
commit
7d9044ab29
202 changed files with 10755 additions and 10972 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue