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
|
|
@ -162,7 +162,7 @@ export class SyncManager {
|
||||||
// Helper methods
|
// Helper methods
|
||||||
|
|
||||||
private async resolveExchange(exchangeCode: string): Promise<string | null> {
|
private async resolveExchange(exchangeCode: string): Promise<string | null> {
|
||||||
if (!exchangeCode) return null;
|
if (!exchangeCode) {return null;}
|
||||||
|
|
||||||
// Simple mapping - expand this as needed
|
// Simple mapping - expand this as needed
|
||||||
const exchangeMap: Record<string, string> = {
|
const exchangeMap: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function Layout() {
|
||||||
// Determine title from current route
|
// Determine title from current route
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
const path = location.pathname.replace('/', '');
|
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);
|
return path.charAt(0).toUpperCase() + path.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -8,7 +7,7 @@ interface DialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({ open, onOpenChange, children }: DialogProps) {
|
export function Dialog({ open, onOpenChange, children }: DialogProps) {
|
||||||
if (!open) return null;
|
if (!open) {return null;}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,9 @@ export function PortfolioTable() {
|
||||||
size: 120,
|
size: 120,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const value = getValue() as number;
|
const value = getValue() as number;
|
||||||
if (value >= 1e12) return <span className="font-mono">${(value / 1e12).toFixed(2)}T</span>;
|
if (value >= 1e12) {return <span className="font-mono">${(value / 1e12).toFixed(2)}T</span>;}
|
||||||
if (value >= 1e9) return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;
|
if (value >= 1e9) {return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;}
|
||||||
if (value >= 1e6) return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;
|
if (value >= 1e6) {return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;}
|
||||||
return <span className="font-mono">${value.toLocaleString()}</span>;
|
return <span className="font-mono">${value.toLocaleString()}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -327,8 +327,8 @@ export function PortfolioTable() {
|
||||||
size: 120,
|
size: 120,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const value = getValue() as number;
|
const value = getValue() as number;
|
||||||
if (value >= 1e9) return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;
|
if (value >= 1e9) {return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;}
|
||||||
if (value >= 1e6) return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;
|
if (value >= 1e6) {return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;}
|
||||||
return <span className="font-mono">${value.toLocaleString()}</span>;
|
return <span className="font-mono">${value.toLocaleString()}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -339,8 +339,8 @@ export function PortfolioTable() {
|
||||||
size: 120,
|
size: 120,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const value = getValue() as number;
|
const value = getValue() as number;
|
||||||
if (value >= 1e9) return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;
|
if (value >= 1e9) {return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;}
|
||||||
if (value >= 1e6) return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;
|
if (value >= 1e6) {return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;}
|
||||||
return <span className="font-mono">${value.toLocaleString()}</span>;
|
return <span className="font-mono">${value.toLocaleString()}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ export function AddProviderMappingDialog({
|
||||||
const exchange = unmappedExchanges.find(
|
const exchange = unmappedExchanges.find(
|
||||||
ex => ex.provider_exchange_code === selectedProviderExchange
|
ex => ex.provider_exchange_code === selectedProviderExchange
|
||||||
);
|
);
|
||||||
if (!exchange) return null;
|
if (!exchange) {return null;}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1 text-xs">
|
<div className="space-y-1 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function AddSourceDialog({
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!source || !sourceCode || !id || !name || !code) return;
|
if (!source || !sourceCode || !id || !name || !code) {return;}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -52,8 +52,8 @@ export function AddSourceDialog({
|
||||||
setName('');
|
setName('');
|
||||||
setCode('');
|
setCode('');
|
||||||
setAliases('');
|
setAliases('');
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.error('Error adding source:', error);
|
console.error('Error adding source:', _error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export function useFormValidation<T>(
|
||||||
onSuccess?: () => void,
|
onSuccess?: () => void,
|
||||||
onError?: (error: unknown) => void
|
onError?: (error: unknown) => void
|
||||||
) => {
|
) => {
|
||||||
if (!validate()) return;
|
if (!validate()) {return;}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export function formatPercentage(value: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getValueColor(value: number): string {
|
export function getValueColor(value: number): string {
|
||||||
if (value > 0) return 'text-success';
|
if (value > 0) {return 'text-success';}
|
||||||
if (value < 0) return 'text-danger';
|
if (value < 0) {return 'text-danger';}
|
||||||
return 'text-text-secondary';
|
return 'text-text-secondary';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@ export function formatPercentage(value: number, decimals = 2): string {
|
||||||
* Format large numbers with K, M, B suffixes
|
* Format large numbers with K, M, B suffixes
|
||||||
*/
|
*/
|
||||||
export function formatNumber(num: number): string {
|
export function formatNumber(num: number): string {
|
||||||
if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B';
|
if (num >= 1e9) {return (num / 1e9).toFixed(1) + 'B';}
|
||||||
if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M';
|
if (num >= 1e6) {return (num / 1e6).toFixed(1) + 'M';}
|
||||||
if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K';
|
if (num >= 1e3) {return (num / 1e3).toFixed(1) + 'K';}
|
||||||
return num.toString();
|
return num.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,8 +33,8 @@ export function formatNumber(num: number): string {
|
||||||
* Get color class based on numeric value (profit/loss)
|
* Get color class based on numeric value (profit/loss)
|
||||||
*/
|
*/
|
||||||
export function getValueColor(value: number): string {
|
export function getValueColor(value: number): string {
|
||||||
if (value > 0) return 'text-success';
|
if (value > 0) {return 'text-success';}
|
||||||
if (value < 0) return 'text-danger';
|
if (value < 0) {return 'text-danger';}
|
||||||
return 'text-text-secondary';
|
return 'text-text-secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,6 +42,6 @@ export function getValueColor(value: number): string {
|
||||||
* Truncate text to specified length
|
* Truncate text to specified length
|
||||||
*/
|
*/
|
||||||
export function truncateText(text: string, length: number): string {
|
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) + '...';
|
return text.slice(0, length) + '...';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,16 +110,16 @@ export class EnvLoader implements ConfigLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle booleans
|
// Handle booleans
|
||||||
if (value.toLowerCase() === 'true') return true;
|
if (value.toLowerCase() === 'true') {return true;}
|
||||||
if (value.toLowerCase() === 'false') return false;
|
if (value.toLowerCase() === 'false') {return false;}
|
||||||
|
|
||||||
// Handle numbers
|
// Handle numbers
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (!isNaN(num) && value !== '') return num;
|
if (!isNaN(num) && value !== '') {return num;}
|
||||||
|
|
||||||
// Handle null/undefined
|
// Handle null/undefined
|
||||||
if (value.toLowerCase() === 'null') return null;
|
if (value.toLowerCase() === 'null') {return null;}
|
||||||
if (value.toLowerCase() === 'undefined') return undefined;
|
if (value.toLowerCase() === 'undefined') {return undefined;}
|
||||||
|
|
||||||
// Return as string
|
// Return as string
|
||||||
return value;
|
return value;
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export class QueueRateLimiter {
|
||||||
r.handler === handler &&
|
r.handler === handler &&
|
||||||
r.operation === operation
|
r.operation === operation
|
||||||
);
|
);
|
||||||
if (rule) return rule;
|
if (rule) {return rule;}
|
||||||
|
|
||||||
// 2. Check for handler-specific rule
|
// 2. Check for handler-specific rule
|
||||||
rule = this.rules.find(r =>
|
rule = this.rules.find(r =>
|
||||||
|
|
@ -101,14 +101,14 @@ export class QueueRateLimiter {
|
||||||
r.queueName === queueName &&
|
r.queueName === queueName &&
|
||||||
r.handler === handler
|
r.handler === handler
|
||||||
);
|
);
|
||||||
if (rule) return rule;
|
if (rule) {return rule;}
|
||||||
|
|
||||||
// 3. Check for queue-specific rule
|
// 3. Check for queue-specific rule
|
||||||
rule = this.rules.find(r =>
|
rule = this.rules.find(r =>
|
||||||
r.level === 'queue' &&
|
r.level === 'queue' &&
|
||||||
r.queueName === queueName
|
r.queueName === queueName
|
||||||
);
|
);
|
||||||
if (rule) return rule;
|
if (rule) {return rule;}
|
||||||
|
|
||||||
// 4. Check for global rule (least specific)
|
// 4. Check for global rule (least specific)
|
||||||
rule = this.rules.find(r => r.level === 'global');
|
rule = this.rules.find(r => r.level === 'global');
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ describe('QueueRateLimiter', () => {
|
||||||
|
|
||||||
// Consume the limit
|
// Consume the limit
|
||||||
await rateLimiter.checkLimit('reset-test', 'operation');
|
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);
|
expect(blocked.allowed).toBe(false);
|
||||||
|
|
||||||
// Reset limits
|
// Reset limits
|
||||||
|
|
@ -248,7 +248,7 @@ describe('QueueRateLimiter', () => {
|
||||||
|
|
||||||
// Verify rule exists
|
// Verify rule exists
|
||||||
await rateLimiter.checkLimit('remove-test', 'op');
|
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);
|
expect(blocked.allowed).toBe(false);
|
||||||
|
|
||||||
// Remove rule
|
// Remove rule
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,11 @@ export {
|
||||||
massIndex,
|
massIndex,
|
||||||
coppockCurve
|
coppockCurve
|
||||||
} from './technical-indicators';
|
} from './technical-indicators';
|
||||||
// export * from './risk-metrics';
|
export * from './risk-metrics';
|
||||||
// export * from './portfolio-analytics';
|
// export * from './portfolio-analytics';
|
||||||
// export * from './options-pricing';
|
// export * from './options-pricing';
|
||||||
// export * from './position-sizing';
|
// export * from './position-sizing';
|
||||||
// export * from './performance-metrics';
|
export * from './performance-metrics';
|
||||||
// export * from './market-statistics';
|
// export * from './market-statistics';
|
||||||
// export * from './volatility-models';
|
// export * from './volatility-models';
|
||||||
// export * from './correlation-analysis';
|
// export * from './correlation-analysis';
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,22 @@
|
||||||
* Comprehensive performance measurement tools for trading strategies and portfolios
|
* 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 {
|
export interface TradePerformance {
|
||||||
totalTrades: number;
|
totalTrades: number;
|
||||||
|
|
@ -140,8 +155,11 @@ export function analyzeDrawdowns(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let peak = equityCurve[0].value;
|
const first = equityCurve[0];
|
||||||
let peakDate = equityCurve[0].date;
|
if (!first) {return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] };}
|
||||||
|
|
||||||
|
let peak = first.value;
|
||||||
|
let peakDate = first.date;
|
||||||
let maxDrawdown = 0;
|
let maxDrawdown = 0;
|
||||||
let maxDrawdownDuration = 0;
|
let maxDrawdownDuration = 0;
|
||||||
|
|
||||||
|
|
@ -157,19 +175,23 @@ export function analyzeDrawdowns(
|
||||||
|
|
||||||
for (let i = 1; i < equityCurve.length; i++) {
|
for (let i = 1; i < equityCurve.length; i++) {
|
||||||
const current = equityCurve[i];
|
const current = equityCurve[i];
|
||||||
|
if (!current) {continue;}
|
||||||
|
|
||||||
if (current.value > peak) {
|
if (current.value > peak) {
|
||||||
// New peak - end any current drawdown
|
// New peak - end any current drawdown
|
||||||
if (currentDrawdownStart) {
|
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(
|
const duration = Math.floor(
|
||||||
(equityCurve[i - 1].date.getTime() - currentDrawdownStart.getTime()) /
|
(prev.date.getTime() - currentDrawdownStart.getTime()) /
|
||||||
(1000 * 60 * 60 * 24)
|
(1000 * 60 * 60 * 24)
|
||||||
);
|
);
|
||||||
|
|
||||||
drawdownPeriods.push({
|
drawdownPeriods.push({
|
||||||
start: currentDrawdownStart,
|
start: currentDrawdownStart,
|
||||||
end: equityCurve[i - 1].date,
|
end: prev.date,
|
||||||
duration,
|
duration,
|
||||||
magnitude: drawdownMagnitude,
|
magnitude: drawdownMagnitude,
|
||||||
});
|
});
|
||||||
|
|
@ -195,6 +217,8 @@ export function analyzeDrawdowns(
|
||||||
// Handle ongoing drawdown
|
// Handle ongoing drawdown
|
||||||
if (currentDrawdownStart) {
|
if (currentDrawdownStart) {
|
||||||
const lastPoint = equityCurve[equityCurve.length - 1];
|
const lastPoint = equityCurve[equityCurve.length - 1];
|
||||||
|
if (!lastPoint) {return { maxDrawdown, maxDrawdownDuration, averageDrawdown: 0, drawdownPeriods };}
|
||||||
|
|
||||||
const drawdownMagnitude = (peak - lastPoint.value) / peak;
|
const drawdownMagnitude = (peak - lastPoint.value) / peak;
|
||||||
const duration = Math.floor(
|
const duration = Math.floor(
|
||||||
(lastPoint.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)
|
(lastPoint.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
|
@ -352,8 +376,10 @@ export function strategyPerformanceAttribution(
|
||||||
|
|
||||||
for (let i = 0; i < sectorWeights.length; i++) {
|
for (let i = 0; i < sectorWeights.length; i++) {
|
||||||
const portfolioWeight = sectorWeights[i];
|
const portfolioWeight = sectorWeights[i];
|
||||||
const benchmarkWeight = 1 / sectorWeights.length; // Assuming equal benchmark weights
|
|
||||||
const sectorReturn = sectorReturns[i];
|
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)
|
// Allocation effect: (portfolio weight - benchmark weight) * (benchmark sector return - benchmark return)
|
||||||
allocationEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - benchmarkReturn);
|
allocationEffect += (portfolioWeight - benchmarkWeight) * (sectorReturn - benchmarkReturn);
|
||||||
|
|
@ -454,15 +480,23 @@ export function calculateStrategyMetrics(
|
||||||
|
|
||||||
const returns = [];
|
const returns = [];
|
||||||
for (let i = 1; i < equityCurve.length; i++) {
|
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);
|
returns.push(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalValue = equityCurve[equityCurve.length - 1].value;
|
const lastPoint = equityCurve[equityCurve.length - 1];
|
||||||
const totalReturn = totalValue - equityCurve[0].value;
|
const firstPoint = equityCurve[0];
|
||||||
const totalReturnPercent = (totalReturn / equityCurve[0].value) * 100;
|
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 dailyReturnPercent = dailyReturn * 100;
|
||||||
|
|
||||||
const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown;
|
const maxDrawdown = analyzeDrawdowns(equityCurve).maxDrawdown;
|
||||||
|
|
@ -528,7 +562,10 @@ export function informationRatio(portfolioReturns: number[], benchmarkReturns: n
|
||||||
}
|
}
|
||||||
|
|
||||||
const excessReturns = portfolioReturns.map(
|
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 trackingError = calculateVolatility(excessReturns);
|
||||||
const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
|
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;
|
return trackingError === 0 ? 0 : avgExcessReturn / trackingError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Treynor Ratio is already exported from risk-metrics
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate Jensen's Alpha (same as Alpha, but included for clarity)
|
* Calculate Jensen's Alpha (same as Alpha, but included for clarity)
|
||||||
|
|
@ -575,11 +599,15 @@ export function captureRatio(
|
||||||
let downMarketPeriods = 0;
|
let downMarketPeriods = 0;
|
||||||
|
|
||||||
for (let i = 0; i < portfolioReturns.length; i++) {
|
for (let i = 0; i < portfolioReturns.length; i++) {
|
||||||
if (benchmarkReturns[i] > 0) {
|
const benchmarkReturn = benchmarkReturns[i];
|
||||||
upCapture += portfolioReturns[i];
|
const portfolioReturn = portfolioReturns[i];
|
||||||
|
if (benchmarkReturn === undefined || portfolioReturn === undefined) {continue;}
|
||||||
|
|
||||||
|
if (benchmarkReturn > 0) {
|
||||||
|
upCapture += portfolioReturn;
|
||||||
upMarketPeriods++;
|
upMarketPeriods++;
|
||||||
} else if (benchmarkReturns[i] < 0) {
|
} else if (benchmarkReturn < 0) {
|
||||||
downCapture += portfolioReturns[i];
|
downCapture += portfolioReturn;
|
||||||
downMarketPeriods++;
|
downMarketPeriods++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -701,11 +729,20 @@ export function calculateRollingAlpha(
|
||||||
export function timeWeightedRateOfReturn(
|
export function timeWeightedRateOfReturn(
|
||||||
cashFlows: Array<{ amount: number; date: Date; value: number }>
|
cashFlows: Array<{ amount: number; date: Date; value: number }>
|
||||||
): number {
|
): number {
|
||||||
|
if (cashFlows.length < 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = cashFlows[0];
|
||||||
|
if (!first) {return 0;}
|
||||||
|
|
||||||
let totalReturn = 1;
|
let totalReturn = 1;
|
||||||
let previousValue = cashFlows[0].value;
|
let previousValue = first.value;
|
||||||
|
|
||||||
for (let i = 1; i < cashFlows.length; i++) {
|
for (let i = 1; i < cashFlows.length; i++) {
|
||||||
const current = cashFlows[i];
|
const current = cashFlows[i];
|
||||||
|
if (!current) {continue;}
|
||||||
|
|
||||||
const periodReturn =
|
const periodReturn =
|
||||||
(current.value - previousValue - current.amount) / (previousValue + current.amount);
|
(current.value - previousValue - current.amount) / (previousValue + current.amount);
|
||||||
totalReturn *= 1 + periodReturn;
|
totalReturn *= 1 + periodReturn;
|
||||||
|
|
@ -721,13 +758,20 @@ export function timeWeightedRateOfReturn(
|
||||||
export function moneyWeightedRateOfReturn(
|
export function moneyWeightedRateOfReturn(
|
||||||
cashFlows: Array<{ amount: number; date: Date; value: number }>
|
cashFlows: Array<{ amount: number; date: Date; value: number }>
|
||||||
): number {
|
): number {
|
||||||
|
if (cashFlows.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = cashFlows[0];
|
||||||
|
if (!first) {return 0;}
|
||||||
|
|
||||||
// Approximate MWRR using Internal Rate of Return (IRR)
|
// Approximate MWRR using Internal Rate of Return (IRR)
|
||||||
// This requires a numerical method or library for accurate IRR calculation
|
// This requires a numerical method or library for accurate IRR calculation
|
||||||
// This is a simplified example and may not be accurate for all cases
|
// This is a simplified example and may not be accurate for all cases
|
||||||
|
|
||||||
let totalCashFlow = 0;
|
let totalCashFlow = 0;
|
||||||
let totalWeightedCashFlow = 0;
|
let totalWeightedCashFlow = 0;
|
||||||
const startDate = cashFlows[0].date.getTime();
|
const startDate = first.date.getTime();
|
||||||
|
|
||||||
for (const cf of cashFlows) {
|
for (const cf of cashFlows) {
|
||||||
const timeDiff = (cf.date.getTime() - startDate) / (1000 * 60 * 60 * 24 * 365); // Years
|
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
|
// Simplified approximation: MWRR ≈ totalCashFlow / totalWeightedCashFlow - 1
|
||||||
return totalCashFlow / totalWeightedCashFlow - 1;
|
return totalWeightedCashFlow === 0 ? 0 : totalCashFlow / totalWeightedCashFlow - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
@ -779,8 +823,12 @@ function calculateBeta(portfolioReturns: number[], marketReturns: number[]): num
|
||||||
let marketVariance = 0;
|
let marketVariance = 0;
|
||||||
|
|
||||||
for (let i = 0; i < portfolioReturns.length; i++) {
|
for (let i = 0; i < portfolioReturns.length; i++) {
|
||||||
const portfolioDiff = portfolioReturns[i] - portfolioMean;
|
const portfolioReturn = portfolioReturns[i];
|
||||||
const marketDiff = marketReturns[i] - marketMean;
|
const marketReturn = marketReturns[i];
|
||||||
|
if (portfolioReturn === undefined || marketReturn === undefined) {continue;}
|
||||||
|
|
||||||
|
const portfolioDiff = portfolioReturn - portfolioMean;
|
||||||
|
const marketDiff = marketReturn - marketMean;
|
||||||
|
|
||||||
covariance += portfolioDiff * marketDiff;
|
covariance += portfolioDiff * marketDiff;
|
||||||
marketVariance += marketDiff * marketDiff;
|
marketVariance += marketDiff * marketDiff;
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Comprehensive risk measurement tools for portfolio and trading analysis
|
* 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
|
* 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);
|
const cutoffIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length);
|
||||||
|
|
||||||
if (cutoffIndex === 0) {
|
if (cutoffIndex === 0) {
|
||||||
return sortedReturns[0];
|
return sortedReturns[0] || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tailReturns = sortedReturns.slice(0, cutoffIndex);
|
const tailReturns = sortedReturns.slice(0, cutoffIndex);
|
||||||
|
|
@ -70,13 +70,19 @@ export function maxDrawdown(equityCurve: number[]): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxDD = 0;
|
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++) {
|
for (let i = 1; i < equityCurve.length; i++) {
|
||||||
if (equityCurve[i] > peak) {
|
const current = equityCurve[i];
|
||||||
peak = equityCurve[i];
|
if (current === undefined) {continue;}
|
||||||
|
|
||||||
|
if (current > peak) {
|
||||||
|
peak = current;
|
||||||
} else {
|
} else {
|
||||||
const drawdown = (peak - equityCurve[i]) / peak;
|
const drawdown = (peak - current) / peak;
|
||||||
maxDD = Math.max(maxDD, drawdown);
|
maxDD = Math.max(maxDD, drawdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,8 +148,12 @@ export function beta(portfolioReturns: number[], marketReturns: number[]): numbe
|
||||||
let marketVariance = 0;
|
let marketVariance = 0;
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const portfolioDiff = portfolioReturns[i] - portfolioMean;
|
const portfolioReturn = portfolioReturns[i];
|
||||||
const marketDiff = marketReturns[i] - marketMean;
|
const marketReturn = marketReturns[i];
|
||||||
|
if (portfolioReturn === undefined || marketReturn === undefined) {continue;}
|
||||||
|
|
||||||
|
const portfolioDiff = portfolioReturn - portfolioMean;
|
||||||
|
const marketDiff = marketReturn - marketMean;
|
||||||
|
|
||||||
covariance += portfolioDiff * marketDiff;
|
covariance += portfolioDiff * marketDiff;
|
||||||
marketVariance += marketDiff * marketDiff;
|
marketVariance += marketDiff * marketDiff;
|
||||||
|
|
@ -168,6 +178,24 @@ export function alpha(
|
||||||
return portfolioMean - (riskFreeRate + portfolioBeta * (marketMean - riskFreeRate));
|
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
|
* Calculate tracking error
|
||||||
*/
|
*/
|
||||||
|
|
@ -176,7 +204,10 @@ export function trackingError(portfolioReturns: number[], benchmarkReturns: numb
|
||||||
return 0;
|
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 mean = activeReturns.reduce((sum, ret) => sum + ret, 0) / activeReturns.length;
|
||||||
|
|
||||||
const variance =
|
const variance =
|
||||||
|
|
@ -380,13 +411,22 @@ export function riskContribution(
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
let marginalContribution = 0;
|
let marginalContribution = 0;
|
||||||
|
const row = covarianceMatrix[i];
|
||||||
|
if (!row) {continue;}
|
||||||
|
|
||||||
for (let j = 0; j < n; j++) {
|
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);
|
const weight = weights[i];
|
||||||
contributions.push(contribution);
|
if (weight !== undefined && portfolioVolatility !== 0) {
|
||||||
|
const contribution = (weight * marginalContribution) / Math.pow(portfolioVolatility, 2);
|
||||||
|
contributions.push(contribution);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return contributions;
|
return contributions;
|
||||||
|
|
@ -396,8 +436,15 @@ export function riskContribution(
|
||||||
* Calculate Ulcer Index
|
* Calculate Ulcer Index
|
||||||
*/
|
*/
|
||||||
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
|
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
|
||||||
|
if (equityCurve.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
let sumSquaredDrawdown = 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) {
|
for (const point of equityCurve) {
|
||||||
peak = Math.max(peak, point.value);
|
peak = Math.max(peak, point.value);
|
||||||
|
|
@ -540,7 +540,7 @@ export function adx(
|
||||||
for (let i = 1; i < ohlcv.length; i++) {
|
for (let i = 1; i < ohlcv.length; i++) {
|
||||||
const current = ohlcv[i];
|
const current = ohlcv[i];
|
||||||
const previous = ohlcv[i - 1];
|
const previous = ohlcv[i - 1];
|
||||||
if (!current || !previous) continue;
|
if (!current || !previous) {continue;}
|
||||||
|
|
||||||
// True Range
|
// True Range
|
||||||
const tr = Math.max(
|
const tr = Math.max(
|
||||||
|
|
@ -575,7 +575,7 @@ export function adx(
|
||||||
const atr = atrValues[i];
|
const atr = atrValues[i];
|
||||||
const plusDMSmoothed = smoothedPlusDM[i];
|
const plusDMSmoothed = smoothedPlusDM[i];
|
||||||
const minusDMSmoothed = smoothedMinusDM[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 diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0;
|
||||||
const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0;
|
const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0;
|
||||||
|
|
@ -612,7 +612,7 @@ export function parabolicSAR(
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = ohlcv[0];
|
const first = ohlcv[0];
|
||||||
if (!first) return [];
|
if (!first) {return [];}
|
||||||
|
|
||||||
const result: number[] = [];
|
const result: number[] = [];
|
||||||
let trend = 1; // 1 for uptrend, -1 for downtrend
|
let trend = 1; // 1 for uptrend, -1 for downtrend
|
||||||
|
|
@ -625,7 +625,7 @@ export function parabolicSAR(
|
||||||
for (let i = 1; i < ohlcv.length; i++) {
|
for (let i = 1; i < ohlcv.length; i++) {
|
||||||
const curr = ohlcv[i];
|
const curr = ohlcv[i];
|
||||||
const prev = ohlcv[i - 1];
|
const prev = ohlcv[i - 1];
|
||||||
if (!curr || !prev) continue;
|
if (!curr || !prev) {continue;}
|
||||||
|
|
||||||
// Calculate new SAR
|
// Calculate new SAR
|
||||||
sar = sar + acceleration * (extremePoint - sar);
|
sar = sar + acceleration * (extremePoint - sar);
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,13 @@ import {
|
||||||
dynamicPositionSize,
|
dynamicPositionSize,
|
||||||
equalWeightPositionSize,
|
equalWeightPositionSize,
|
||||||
expectancyPositionSize,
|
expectancyPositionSize,
|
||||||
fixedFractionalPositionSize,
|
|
||||||
fixedRiskPositionSize,
|
fixedRiskPositionSize,
|
||||||
fractionalKellyPositionSize,
|
|
||||||
kellyPositionSize,
|
kellyPositionSize,
|
||||||
liquidityConstrainedPositionSize,
|
liquidityConstrainedPositionSize,
|
||||||
monteCarloPositionSize,
|
|
||||||
multiTimeframePositionSize,
|
multiTimeframePositionSize,
|
||||||
riskParityPositionSize,
|
riskParityPositionSize,
|
||||||
sharpeOptimizedPositionSize,
|
sharpeOptimizedPositionSize,
|
||||||
validatePositionSize,
|
validatePositionSize,
|
||||||
volatilityAdjustedPositionSize,
|
|
||||||
volatilityTargetPositionSize,
|
volatilityTargetPositionSize,
|
||||||
type KellyParams,
|
type KellyParams,
|
||||||
type PositionSizeParams,
|
type PositionSizeParams,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue