linxus fs fixes
This commit is contained in:
parent
ac23b70146
commit
0b7846fe67
292 changed files with 41947 additions and 41947 deletions
|
|
@ -1,403 +1,403 @@
|
|||
/**
|
||||
* Test suite for position sizing calculations
|
||||
*/
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
fixedRiskPositionSize,
|
||||
kellyPositionSize,
|
||||
fractionalKellyPositionSize,
|
||||
volatilityTargetPositionSize,
|
||||
equalWeightPositionSize,
|
||||
atrBasedPositionSize,
|
||||
expectancyPositionSize,
|
||||
monteCarloPositionSize,
|
||||
sharpeOptimizedPositionSize,
|
||||
fixedFractionalPositionSize,
|
||||
volatilityAdjustedPositionSize,
|
||||
correlationAdjustedPositionSize,
|
||||
calculatePortfolioHeat,
|
||||
dynamicPositionSize,
|
||||
liquidityConstrainedPositionSize,
|
||||
multiTimeframePositionSize,
|
||||
riskParityPositionSize,
|
||||
validatePositionSize,
|
||||
type PositionSizeParams,
|
||||
type KellyParams,
|
||||
type VolatilityParams
|
||||
} from '../../src/calculations/position-sizing';
|
||||
|
||||
describe('Position Sizing Calculations', () => {
|
||||
describe('fixedRiskPositionSize', () => {
|
||||
it('should calculate correct position size for long position', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 100000,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 95,
|
||||
leverage: 1
|
||||
};
|
||||
|
||||
const result = fixedRiskPositionSize(params);
|
||||
// Risk amount: 100000 * 0.02 = 2000
|
||||
// Risk per share: 100 - 95 = 5
|
||||
// Position size: 2000 / 5 = 400 shares
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should calculate correct position size for short position', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 100000,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 105,
|
||||
leverage: 1
|
||||
};
|
||||
|
||||
const result = fixedRiskPositionSize(params);
|
||||
// Risk per share: |100 - 105| = 5
|
||||
// Position size: 2000 / 5 = 400 shares
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 0,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 95
|
||||
};
|
||||
|
||||
expect(fixedRiskPositionSize(params)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 when entry price equals stop loss', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 100000,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 100
|
||||
};
|
||||
|
||||
expect(fixedRiskPositionSize(params)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('kellyPositionSize', () => {
|
||||
it('should calculate correct Kelly position size', () => {
|
||||
const params: KellyParams = {
|
||||
winRate: 0.6,
|
||||
averageWin: 150,
|
||||
averageLoss: -100
|
||||
};
|
||||
|
||||
const result = kellyPositionSize(params, 100000);
|
||||
|
||||
// Kelly formula: f = (bp - q) / b
|
||||
// b = 150/100 = 1.5, p = 0.6, q = 0.4
|
||||
// f = (1.5 * 0.6 - 0.4) / 1.5 = (0.9 - 0.4) / 1.5 = 0.5 / 1.5 = 0.333
|
||||
// With safety factor of 0.25: 0.333 * 0.25 = 0.083
|
||||
// Capped at 0.25, so result should be 0.083
|
||||
// Position: 100000 * 0.083 = 8300
|
||||
expect(result).toBeCloseTo(8333, 0);
|
||||
});
|
||||
|
||||
it('should return 0 for negative expectancy', () => {
|
||||
const params: KellyParams = {
|
||||
winRate: 0.3,
|
||||
averageWin: 100,
|
||||
averageLoss: -200
|
||||
};
|
||||
|
||||
const result = kellyPositionSize(params, 100000);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
const params: KellyParams = {
|
||||
winRate: 0,
|
||||
averageWin: 100,
|
||||
averageLoss: -100
|
||||
};
|
||||
|
||||
expect(kellyPositionSize(params, 100000)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('volatilityTargetPositionSize', () => {
|
||||
it('should calculate correct volatility-targeted position size', () => {
|
||||
const params: VolatilityParams = {
|
||||
price: 100,
|
||||
volatility: 0.20,
|
||||
targetVolatility: 0.10,
|
||||
lookbackDays: 30
|
||||
};
|
||||
|
||||
const result = volatilityTargetPositionSize(params, 100000);
|
||||
|
||||
// Volatility ratio: 0.10 / 0.20 = 0.5
|
||||
// Position value: 100000 * 0.5 = 50000
|
||||
// Position size: 50000 / 100 = 500 shares
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
|
||||
it('should cap leverage at 2x', () => {
|
||||
const params: VolatilityParams = {
|
||||
price: 100,
|
||||
volatility: 0.05,
|
||||
targetVolatility: 0.20,
|
||||
lookbackDays: 30
|
||||
};
|
||||
|
||||
const result = volatilityTargetPositionSize(params, 100000);
|
||||
|
||||
// Volatility ratio would be 4, but capped at 2
|
||||
// Position value: 100000 * 2 = 200000
|
||||
// Position size: 200000 / 100 = 2000 shares
|
||||
expect(result).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equalWeightPositionSize', () => {
|
||||
it('should calculate equal weight position size', () => {
|
||||
const result = equalWeightPositionSize(100000, 5, 100);
|
||||
|
||||
// Position value per asset: 100000 / 5 = 20000
|
||||
// Position size: 20000 / 100 = 200 shares
|
||||
expect(result).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
expect(equalWeightPositionSize(100000, 0, 100)).toBe(0);
|
||||
expect(equalWeightPositionSize(100000, 5, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('atrBasedPositionSize', () => {
|
||||
it('should calculate ATR-based position size', () => {
|
||||
const result = atrBasedPositionSize(100000, 2, 5, 2, 100);
|
||||
|
||||
// Risk amount: 100000 * 0.02 = 2000
|
||||
// Stop distance: 5 * 2 = 10
|
||||
// Position size: 2000 / 10 = 200 shares
|
||||
expect(result).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 0 for zero ATR', () => {
|
||||
const result = atrBasedPositionSize(100000, 2, 0, 2, 100);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expectancyPositionSize', () => {
|
||||
it('should calculate expectancy-based position size', () => {
|
||||
const result = expectancyPositionSize(100000, 0.6, 150, -100, 5);
|
||||
|
||||
// Expectancy: 0.6 * 150 - 0.4 * 100 = 90 - 40 = 50
|
||||
// Expectancy ratio: 50 / 100 = 0.5
|
||||
// Risk percentage: min(0.5 * 0.5, 5) = min(0.25, 5) = 0.25
|
||||
// Position: 100000 * 0.0025 = 250
|
||||
expect(result).toBe(250);
|
||||
});
|
||||
|
||||
it('should return 0 for negative expectancy', () => {
|
||||
const result = expectancyPositionSize(100000, 0.3, 100, -200);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('correlationAdjustedPositionSize', () => {
|
||||
it('should adjust position size based on correlation', () => {
|
||||
const existingPositions = [
|
||||
{ size: 1000, correlation: 0.5 },
|
||||
{ size: 500, correlation: 0.3 }
|
||||
];
|
||||
|
||||
const result = correlationAdjustedPositionSize(1000, existingPositions, 0.5);
|
||||
|
||||
// Should reduce position size based on correlation risk
|
||||
expect(result).toBeLessThan(1000);
|
||||
expect(result).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return original size when no existing positions', () => {
|
||||
const result = correlationAdjustedPositionSize(1000, [], 0.5);
|
||||
expect(result).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculatePortfolioHeat', () => {
|
||||
it('should calculate portfolio heat correctly', () => {
|
||||
const positions = [
|
||||
{ value: 10000, risk: 500 },
|
||||
{ value: 15000, risk: 750 },
|
||||
{ value: 20000, risk: 1000 }
|
||||
];
|
||||
|
||||
const result = calculatePortfolioHeat(positions, 100000);
|
||||
|
||||
// Total risk: 500 + 750 + 1000 = 2250
|
||||
// Heat: (2250 / 100000) * 100 = 2.25%
|
||||
expect(result).toBe(2.25);
|
||||
});
|
||||
|
||||
it('should handle empty positions array', () => {
|
||||
const result = calculatePortfolioHeat([], 100000);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should cap heat at 100%', () => {
|
||||
const positions = [
|
||||
{ value: 50000, risk: 150000 }
|
||||
];
|
||||
|
||||
const result = calculatePortfolioHeat(positions, 100000);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamicPositionSize', () => {
|
||||
it('should adjust position size based on market conditions', () => {
|
||||
const result = dynamicPositionSize(1000, 0.25, 0.15, 0.05, 0.10);
|
||||
|
||||
// Volatility adjustment: 0.15 / 0.25 = 0.6
|
||||
// Drawdown adjustment: 1 - (0.05 / 0.10) = 0.5
|
||||
// Adjusted size: 1000 * 0.6 * 0.5 = 300
|
||||
expect(result).toBe(300);
|
||||
});
|
||||
|
||||
it('should handle high drawdown', () => {
|
||||
const result = dynamicPositionSize(1000, 0.20, 0.15, 0.15, 0.10);
|
||||
|
||||
// Should significantly reduce position size due to high drawdown
|
||||
expect(result).toBeLessThan(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('liquidityConstrainedPositionSize', () => {
|
||||
it('should constrain position size based on liquidity', () => {
|
||||
const result = liquidityConstrainedPositionSize(1000, 10000, 0.05, 100);
|
||||
|
||||
// Max shares: 10000 * 0.05 = 500
|
||||
// Should return min(1000, 500) = 500
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
|
||||
it('should return desired size when liquidity allows', () => {
|
||||
const result = liquidityConstrainedPositionSize(500, 20000, 0.05, 100);
|
||||
|
||||
// Max shares: 20000 * 0.05 = 1000
|
||||
// Should return min(500, 1000) = 500
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiTimeframePositionSize', () => {
|
||||
it('should weight signals correctly', () => {
|
||||
const result = multiTimeframePositionSize(100000, 0.8, 0.6, 0.4, 2);
|
||||
|
||||
// Weighted signal: 0.8 * 0.2 + 0.6 * 0.3 + 0.4 * 0.5 = 0.16 + 0.18 + 0.2 = 0.54
|
||||
// Adjusted risk: 2 * 0.54 = 1.08%
|
||||
// Position: 100000 * 0.0108 = 1080
|
||||
expect(result).toBe(1080);
|
||||
});
|
||||
|
||||
it('should clamp signals to valid range', () => {
|
||||
const result = multiTimeframePositionSize(100000, 2, -2, 1.5, 2);
|
||||
|
||||
// Signals should be clamped to [-1, 1]
|
||||
// Weighted: 1 * 0.2 + (-1) * 0.3 + 1 * 0.5 = 0.2 - 0.3 + 0.5 = 0.4
|
||||
// Adjusted risk: 2 * 0.4 = 0.8%
|
||||
expect(result).toBe(800);
|
||||
});
|
||||
});
|
||||
|
||||
describe('riskParityPositionSize', () => {
|
||||
it('should allocate based on inverse volatility', () => {
|
||||
const assets = [
|
||||
{ volatility: 0.10, price: 100 },
|
||||
{ volatility: 0.20, price: 200 }
|
||||
];
|
||||
|
||||
const result = riskParityPositionSize(assets, 0.15, 100000);
|
||||
|
||||
// Asset 1: 1/0.10 = 10, Asset 2: 1/0.20 = 5
|
||||
// Total inverse vol: 15
|
||||
// Weights: Asset 1: 10/15 = 0.667, Asset 2: 5/15 = 0.333
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBeGreaterThan(result[1]);
|
||||
});
|
||||
|
||||
it('should handle zero volatility assets', () => {
|
||||
const assets = [
|
||||
{ volatility: 0, price: 100 },
|
||||
{ volatility: 0.20, price: 200 }
|
||||
];
|
||||
|
||||
const result = riskParityPositionSize(assets, 0.15, 100000);
|
||||
|
||||
expect(result[0]).toBe(0);
|
||||
expect(result[1]).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sharpeOptimizedPositionSize', () => {
|
||||
it('should calculate position size based on Sharpe optimization', () => {
|
||||
const result = sharpeOptimizedPositionSize(100000, 0.15, 0.20, 0.02, 3);
|
||||
|
||||
// Kelly formula for continuous returns: f = (μ - r) / σ²
|
||||
// Expected return: 0.15, Risk-free: 0.02, Volatility: 0.20
|
||||
// f = (0.15 - 0.02) / (0.20)² = 0.13 / 0.04 = 3.25
|
||||
// But capped at maxLeverage=3, so should be 3.0
|
||||
// Final position: 100000 * 3 = 300000
|
||||
expect(result).toBe(300000);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
// Invalid volatility
|
||||
expect(sharpeOptimizedPositionSize(100000, 0.15, 0, 0.02)).toBe(0);
|
||||
|
||||
// Invalid account size
|
||||
expect(sharpeOptimizedPositionSize(0, 0.15, 0.20, 0.02)).toBe(0);
|
||||
|
||||
// Expected return less than risk-free rate
|
||||
expect(sharpeOptimizedPositionSize(100000, 0.01, 0.20, 0.02)).toBe(0);
|
||||
});
|
||||
|
||||
it('should respect maximum leverage', () => {
|
||||
const result = sharpeOptimizedPositionSize(100000, 0.30, 0.20, 0.02, 2);
|
||||
|
||||
// Kelly fraction would be (0.30 - 0.02) / (0.20)² = 7, but capped at 2
|
||||
// Position: 100000 * 2 = 200000
|
||||
expect(result).toBe(200000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePositionSize', () => {
|
||||
it('should validate position size against limits', () => {
|
||||
const result = validatePositionSize(500, 100, 100000, 10, 2);
|
||||
|
||||
// Position value: 500 * 100 = 50000 (50% of account)
|
||||
// This exceeds 10% limit
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.violations).toContain('Position exceeds maximum 10% of account');
|
||||
expect(result.adjustedSize).toBe(100); // 10000 / 100
|
||||
});
|
||||
|
||||
it('should pass validation for reasonable position', () => {
|
||||
const result = validatePositionSize(50, 100, 100000, 10, 2);
|
||||
|
||||
// Position value: 50 * 100 = 5000 (5% of account)
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.violations).toHaveLength(0);
|
||||
expect(result.adjustedSize).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle fractional shares', () => {
|
||||
const result = validatePositionSize(0.5, 100, 100000, 10, 2);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.violations).toContain('Position size too small (less than 1 share)');
|
||||
expect(result.adjustedSize).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Test suite for position sizing calculations
|
||||
*/
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
fixedRiskPositionSize,
|
||||
kellyPositionSize,
|
||||
fractionalKellyPositionSize,
|
||||
volatilityTargetPositionSize,
|
||||
equalWeightPositionSize,
|
||||
atrBasedPositionSize,
|
||||
expectancyPositionSize,
|
||||
monteCarloPositionSize,
|
||||
sharpeOptimizedPositionSize,
|
||||
fixedFractionalPositionSize,
|
||||
volatilityAdjustedPositionSize,
|
||||
correlationAdjustedPositionSize,
|
||||
calculatePortfolioHeat,
|
||||
dynamicPositionSize,
|
||||
liquidityConstrainedPositionSize,
|
||||
multiTimeframePositionSize,
|
||||
riskParityPositionSize,
|
||||
validatePositionSize,
|
||||
type PositionSizeParams,
|
||||
type KellyParams,
|
||||
type VolatilityParams
|
||||
} from '../../src/calculations/position-sizing';
|
||||
|
||||
describe('Position Sizing Calculations', () => {
|
||||
describe('fixedRiskPositionSize', () => {
|
||||
it('should calculate correct position size for long position', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 100000,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 95,
|
||||
leverage: 1
|
||||
};
|
||||
|
||||
const result = fixedRiskPositionSize(params);
|
||||
// Risk amount: 100000 * 0.02 = 2000
|
||||
// Risk per share: 100 - 95 = 5
|
||||
// Position size: 2000 / 5 = 400 shares
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should calculate correct position size for short position', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 100000,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 105,
|
||||
leverage: 1
|
||||
};
|
||||
|
||||
const result = fixedRiskPositionSize(params);
|
||||
// Risk per share: |100 - 105| = 5
|
||||
// Position size: 2000 / 5 = 400 shares
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 0,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 95
|
||||
};
|
||||
|
||||
expect(fixedRiskPositionSize(params)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 when entry price equals stop loss', () => {
|
||||
const params: PositionSizeParams = {
|
||||
accountSize: 100000,
|
||||
riskPercentage: 2,
|
||||
entryPrice: 100,
|
||||
stopLoss: 100
|
||||
};
|
||||
|
||||
expect(fixedRiskPositionSize(params)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('kellyPositionSize', () => {
|
||||
it('should calculate correct Kelly position size', () => {
|
||||
const params: KellyParams = {
|
||||
winRate: 0.6,
|
||||
averageWin: 150,
|
||||
averageLoss: -100
|
||||
};
|
||||
|
||||
const result = kellyPositionSize(params, 100000);
|
||||
|
||||
// Kelly formula: f = (bp - q) / b
|
||||
// b = 150/100 = 1.5, p = 0.6, q = 0.4
|
||||
// f = (1.5 * 0.6 - 0.4) / 1.5 = (0.9 - 0.4) / 1.5 = 0.5 / 1.5 = 0.333
|
||||
// With safety factor of 0.25: 0.333 * 0.25 = 0.083
|
||||
// Capped at 0.25, so result should be 0.083
|
||||
// Position: 100000 * 0.083 = 8300
|
||||
expect(result).toBeCloseTo(8333, 0);
|
||||
});
|
||||
|
||||
it('should return 0 for negative expectancy', () => {
|
||||
const params: KellyParams = {
|
||||
winRate: 0.3,
|
||||
averageWin: 100,
|
||||
averageLoss: -200
|
||||
};
|
||||
|
||||
const result = kellyPositionSize(params, 100000);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
const params: KellyParams = {
|
||||
winRate: 0,
|
||||
averageWin: 100,
|
||||
averageLoss: -100
|
||||
};
|
||||
|
||||
expect(kellyPositionSize(params, 100000)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('volatilityTargetPositionSize', () => {
|
||||
it('should calculate correct volatility-targeted position size', () => {
|
||||
const params: VolatilityParams = {
|
||||
price: 100,
|
||||
volatility: 0.20,
|
||||
targetVolatility: 0.10,
|
||||
lookbackDays: 30
|
||||
};
|
||||
|
||||
const result = volatilityTargetPositionSize(params, 100000);
|
||||
|
||||
// Volatility ratio: 0.10 / 0.20 = 0.5
|
||||
// Position value: 100000 * 0.5 = 50000
|
||||
// Position size: 50000 / 100 = 500 shares
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
|
||||
it('should cap leverage at 2x', () => {
|
||||
const params: VolatilityParams = {
|
||||
price: 100,
|
||||
volatility: 0.05,
|
||||
targetVolatility: 0.20,
|
||||
lookbackDays: 30
|
||||
};
|
||||
|
||||
const result = volatilityTargetPositionSize(params, 100000);
|
||||
|
||||
// Volatility ratio would be 4, but capped at 2
|
||||
// Position value: 100000 * 2 = 200000
|
||||
// Position size: 200000 / 100 = 2000 shares
|
||||
expect(result).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equalWeightPositionSize', () => {
|
||||
it('should calculate equal weight position size', () => {
|
||||
const result = equalWeightPositionSize(100000, 5, 100);
|
||||
|
||||
// Position value per asset: 100000 / 5 = 20000
|
||||
// Position size: 20000 / 100 = 200 shares
|
||||
expect(result).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
expect(equalWeightPositionSize(100000, 0, 100)).toBe(0);
|
||||
expect(equalWeightPositionSize(100000, 5, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('atrBasedPositionSize', () => {
|
||||
it('should calculate ATR-based position size', () => {
|
||||
const result = atrBasedPositionSize(100000, 2, 5, 2, 100);
|
||||
|
||||
// Risk amount: 100000 * 0.02 = 2000
|
||||
// Stop distance: 5 * 2 = 10
|
||||
// Position size: 2000 / 10 = 200 shares
|
||||
expect(result).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 0 for zero ATR', () => {
|
||||
const result = atrBasedPositionSize(100000, 2, 0, 2, 100);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expectancyPositionSize', () => {
|
||||
it('should calculate expectancy-based position size', () => {
|
||||
const result = expectancyPositionSize(100000, 0.6, 150, -100, 5);
|
||||
|
||||
// Expectancy: 0.6 * 150 - 0.4 * 100 = 90 - 40 = 50
|
||||
// Expectancy ratio: 50 / 100 = 0.5
|
||||
// Risk percentage: min(0.5 * 0.5, 5) = min(0.25, 5) = 0.25
|
||||
// Position: 100000 * 0.0025 = 250
|
||||
expect(result).toBe(250);
|
||||
});
|
||||
|
||||
it('should return 0 for negative expectancy', () => {
|
||||
const result = expectancyPositionSize(100000, 0.3, 100, -200);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('correlationAdjustedPositionSize', () => {
|
||||
it('should adjust position size based on correlation', () => {
|
||||
const existingPositions = [
|
||||
{ size: 1000, correlation: 0.5 },
|
||||
{ size: 500, correlation: 0.3 }
|
||||
];
|
||||
|
||||
const result = correlationAdjustedPositionSize(1000, existingPositions, 0.5);
|
||||
|
||||
// Should reduce position size based on correlation risk
|
||||
expect(result).toBeLessThan(1000);
|
||||
expect(result).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return original size when no existing positions', () => {
|
||||
const result = correlationAdjustedPositionSize(1000, [], 0.5);
|
||||
expect(result).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculatePortfolioHeat', () => {
|
||||
it('should calculate portfolio heat correctly', () => {
|
||||
const positions = [
|
||||
{ value: 10000, risk: 500 },
|
||||
{ value: 15000, risk: 750 },
|
||||
{ value: 20000, risk: 1000 }
|
||||
];
|
||||
|
||||
const result = calculatePortfolioHeat(positions, 100000);
|
||||
|
||||
// Total risk: 500 + 750 + 1000 = 2250
|
||||
// Heat: (2250 / 100000) * 100 = 2.25%
|
||||
expect(result).toBe(2.25);
|
||||
});
|
||||
|
||||
it('should handle empty positions array', () => {
|
||||
const result = calculatePortfolioHeat([], 100000);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should cap heat at 100%', () => {
|
||||
const positions = [
|
||||
{ value: 50000, risk: 150000 }
|
||||
];
|
||||
|
||||
const result = calculatePortfolioHeat(positions, 100000);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamicPositionSize', () => {
|
||||
it('should adjust position size based on market conditions', () => {
|
||||
const result = dynamicPositionSize(1000, 0.25, 0.15, 0.05, 0.10);
|
||||
|
||||
// Volatility adjustment: 0.15 / 0.25 = 0.6
|
||||
// Drawdown adjustment: 1 - (0.05 / 0.10) = 0.5
|
||||
// Adjusted size: 1000 * 0.6 * 0.5 = 300
|
||||
expect(result).toBe(300);
|
||||
});
|
||||
|
||||
it('should handle high drawdown', () => {
|
||||
const result = dynamicPositionSize(1000, 0.20, 0.15, 0.15, 0.10);
|
||||
|
||||
// Should significantly reduce position size due to high drawdown
|
||||
expect(result).toBeLessThan(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('liquidityConstrainedPositionSize', () => {
|
||||
it('should constrain position size based on liquidity', () => {
|
||||
const result = liquidityConstrainedPositionSize(1000, 10000, 0.05, 100);
|
||||
|
||||
// Max shares: 10000 * 0.05 = 500
|
||||
// Should return min(1000, 500) = 500
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
|
||||
it('should return desired size when liquidity allows', () => {
|
||||
const result = liquidityConstrainedPositionSize(500, 20000, 0.05, 100);
|
||||
|
||||
// Max shares: 20000 * 0.05 = 1000
|
||||
// Should return min(500, 1000) = 500
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiTimeframePositionSize', () => {
|
||||
it('should weight signals correctly', () => {
|
||||
const result = multiTimeframePositionSize(100000, 0.8, 0.6, 0.4, 2);
|
||||
|
||||
// Weighted signal: 0.8 * 0.2 + 0.6 * 0.3 + 0.4 * 0.5 = 0.16 + 0.18 + 0.2 = 0.54
|
||||
// Adjusted risk: 2 * 0.54 = 1.08%
|
||||
// Position: 100000 * 0.0108 = 1080
|
||||
expect(result).toBe(1080);
|
||||
});
|
||||
|
||||
it('should clamp signals to valid range', () => {
|
||||
const result = multiTimeframePositionSize(100000, 2, -2, 1.5, 2);
|
||||
|
||||
// Signals should be clamped to [-1, 1]
|
||||
// Weighted: 1 * 0.2 + (-1) * 0.3 + 1 * 0.5 = 0.2 - 0.3 + 0.5 = 0.4
|
||||
// Adjusted risk: 2 * 0.4 = 0.8%
|
||||
expect(result).toBe(800);
|
||||
});
|
||||
});
|
||||
|
||||
describe('riskParityPositionSize', () => {
|
||||
it('should allocate based on inverse volatility', () => {
|
||||
const assets = [
|
||||
{ volatility: 0.10, price: 100 },
|
||||
{ volatility: 0.20, price: 200 }
|
||||
];
|
||||
|
||||
const result = riskParityPositionSize(assets, 0.15, 100000);
|
||||
|
||||
// Asset 1: 1/0.10 = 10, Asset 2: 1/0.20 = 5
|
||||
// Total inverse vol: 15
|
||||
// Weights: Asset 1: 10/15 = 0.667, Asset 2: 5/15 = 0.333
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBeGreaterThan(result[1]);
|
||||
});
|
||||
|
||||
it('should handle zero volatility assets', () => {
|
||||
const assets = [
|
||||
{ volatility: 0, price: 100 },
|
||||
{ volatility: 0.20, price: 200 }
|
||||
];
|
||||
|
||||
const result = riskParityPositionSize(assets, 0.15, 100000);
|
||||
|
||||
expect(result[0]).toBe(0);
|
||||
expect(result[1]).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sharpeOptimizedPositionSize', () => {
|
||||
it('should calculate position size based on Sharpe optimization', () => {
|
||||
const result = sharpeOptimizedPositionSize(100000, 0.15, 0.20, 0.02, 3);
|
||||
|
||||
// Kelly formula for continuous returns: f = (μ - r) / σ²
|
||||
// Expected return: 0.15, Risk-free: 0.02, Volatility: 0.20
|
||||
// f = (0.15 - 0.02) / (0.20)² = 0.13 / 0.04 = 3.25
|
||||
// But capped at maxLeverage=3, so should be 3.0
|
||||
// Final position: 100000 * 3 = 300000
|
||||
expect(result).toBe(300000);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid inputs', () => {
|
||||
// Invalid volatility
|
||||
expect(sharpeOptimizedPositionSize(100000, 0.15, 0, 0.02)).toBe(0);
|
||||
|
||||
// Invalid account size
|
||||
expect(sharpeOptimizedPositionSize(0, 0.15, 0.20, 0.02)).toBe(0);
|
||||
|
||||
// Expected return less than risk-free rate
|
||||
expect(sharpeOptimizedPositionSize(100000, 0.01, 0.20, 0.02)).toBe(0);
|
||||
});
|
||||
|
||||
it('should respect maximum leverage', () => {
|
||||
const result = sharpeOptimizedPositionSize(100000, 0.30, 0.20, 0.02, 2);
|
||||
|
||||
// Kelly fraction would be (0.30 - 0.02) / (0.20)² = 7, but capped at 2
|
||||
// Position: 100000 * 2 = 200000
|
||||
expect(result).toBe(200000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePositionSize', () => {
|
||||
it('should validate position size against limits', () => {
|
||||
const result = validatePositionSize(500, 100, 100000, 10, 2);
|
||||
|
||||
// Position value: 500 * 100 = 50000 (50% of account)
|
||||
// This exceeds 10% limit
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.violations).toContain('Position exceeds maximum 10% of account');
|
||||
expect(result.adjustedSize).toBe(100); // 10000 / 100
|
||||
});
|
||||
|
||||
it('should pass validation for reasonable position', () => {
|
||||
const result = validatePositionSize(50, 100, 100000, 10, 2);
|
||||
|
||||
// Position value: 50 * 100 = 5000 (5% of account)
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.violations).toHaveLength(0);
|
||||
expect(result.adjustedSize).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle fractional shares', () => {
|
||||
const result = validatePositionSize(0.5, 100, 100000, 10, 2);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.violations).toContain('Position size too small (less than 1 share)');
|
||||
expect(result.adjustedSize).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,80 +1,80 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { dateUtils } from '../src/dateUtils';
|
||||
|
||||
describe('dateUtils', () => {
|
||||
describe('isTradingDay', () => {
|
||||
it('should return true for weekdays (Monday-Friday)', () => {
|
||||
// Monday (June 2, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 2))).toBe(true);
|
||||
// Tuesday (June 3, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 3))).toBe(true);
|
||||
// Wednesday (June 4, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 4))).toBe(true);
|
||||
// Thursday (June 5, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 5))).toBe(true);
|
||||
// Friday (June 6, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 6))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for weekends (Saturday-Sunday)', () => {
|
||||
// Saturday (June 7, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 7))).toBe(false);
|
||||
// Sunday (June 8, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 8))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextTradingDay', () => {
|
||||
it('should return the next day when current day is a weekday and next day is a weekday', () => {
|
||||
// Monday -> Tuesday
|
||||
const monday = new Date(2025, 5, 2);
|
||||
const tuesday = new Date(2025, 5, 3);
|
||||
expect(dateUtils.getNextTradingDay(monday).toDateString()).toBe(tuesday.toDateString());
|
||||
});
|
||||
|
||||
it('should skip weekends when getting next trading day', () => {
|
||||
// Friday -> Monday
|
||||
const friday = new Date(2025, 5, 6);
|
||||
const monday = new Date(2025, 5, 9);
|
||||
expect(dateUtils.getNextTradingDay(friday).toDateString()).toBe(monday.toDateString());
|
||||
});
|
||||
|
||||
it('should handle weekends as input correctly', () => {
|
||||
// Saturday -> Monday
|
||||
const saturday = new Date(2025, 5, 7);
|
||||
const monday = new Date(2025, 5, 9);
|
||||
expect(dateUtils.getNextTradingDay(saturday).toDateString()).toBe(monday.toDateString());
|
||||
|
||||
// Sunday -> Monday
|
||||
const sunday = new Date(2025, 5, 8);
|
||||
expect(dateUtils.getNextTradingDay(sunday).toDateString()).toBe(monday.toDateString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreviousTradingDay', () => {
|
||||
it('should return the previous day when current day is a weekday and previous day is a weekday', () => {
|
||||
// Tuesday -> Monday
|
||||
const tuesday = new Date(2025, 5, 3);
|
||||
const monday = new Date(2025, 5, 2);
|
||||
expect(dateUtils.getPreviousTradingDay(tuesday).toDateString()).toBe(monday.toDateString());
|
||||
});
|
||||
|
||||
it('should skip weekends when getting previous trading day', () => {
|
||||
// Monday -> Friday
|
||||
const monday = new Date(2025, 5, 9);
|
||||
const friday = new Date(2025, 5, 6);
|
||||
expect(dateUtils.getPreviousTradingDay(monday).toDateString()).toBe(friday.toDateString());
|
||||
});
|
||||
|
||||
it('should handle weekends as input correctly', () => {
|
||||
// Saturday -> Friday
|
||||
const saturday = new Date(2025, 5, 7);
|
||||
const friday = new Date(2025, 5, 6);
|
||||
expect(dateUtils.getPreviousTradingDay(saturday).toDateString()).toBe(friday.toDateString());
|
||||
|
||||
// Sunday -> Friday
|
||||
const sunday = new Date(2025, 5, 8);
|
||||
expect(dateUtils.getPreviousTradingDay(sunday).toDateString()).toBe(friday.toDateString());
|
||||
});
|
||||
});
|
||||
});
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { dateUtils } from '../src/dateUtils';
|
||||
|
||||
describe('dateUtils', () => {
|
||||
describe('isTradingDay', () => {
|
||||
it('should return true for weekdays (Monday-Friday)', () => {
|
||||
// Monday (June 2, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 2))).toBe(true);
|
||||
// Tuesday (June 3, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 3))).toBe(true);
|
||||
// Wednesday (June 4, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 4))).toBe(true);
|
||||
// Thursday (June 5, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 5))).toBe(true);
|
||||
// Friday (June 6, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 6))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for weekends (Saturday-Sunday)', () => {
|
||||
// Saturday (June 7, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 7))).toBe(false);
|
||||
// Sunday (June 8, 2025)
|
||||
expect(dateUtils.isTradingDay(new Date(2025, 5, 8))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextTradingDay', () => {
|
||||
it('should return the next day when current day is a weekday and next day is a weekday', () => {
|
||||
// Monday -> Tuesday
|
||||
const monday = new Date(2025, 5, 2);
|
||||
const tuesday = new Date(2025, 5, 3);
|
||||
expect(dateUtils.getNextTradingDay(monday).toDateString()).toBe(tuesday.toDateString());
|
||||
});
|
||||
|
||||
it('should skip weekends when getting next trading day', () => {
|
||||
// Friday -> Monday
|
||||
const friday = new Date(2025, 5, 6);
|
||||
const monday = new Date(2025, 5, 9);
|
||||
expect(dateUtils.getNextTradingDay(friday).toDateString()).toBe(monday.toDateString());
|
||||
});
|
||||
|
||||
it('should handle weekends as input correctly', () => {
|
||||
// Saturday -> Monday
|
||||
const saturday = new Date(2025, 5, 7);
|
||||
const monday = new Date(2025, 5, 9);
|
||||
expect(dateUtils.getNextTradingDay(saturday).toDateString()).toBe(monday.toDateString());
|
||||
|
||||
// Sunday -> Monday
|
||||
const sunday = new Date(2025, 5, 8);
|
||||
expect(dateUtils.getNextTradingDay(sunday).toDateString()).toBe(monday.toDateString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreviousTradingDay', () => {
|
||||
it('should return the previous day when current day is a weekday and previous day is a weekday', () => {
|
||||
// Tuesday -> Monday
|
||||
const tuesday = new Date(2025, 5, 3);
|
||||
const monday = new Date(2025, 5, 2);
|
||||
expect(dateUtils.getPreviousTradingDay(tuesday).toDateString()).toBe(monday.toDateString());
|
||||
});
|
||||
|
||||
it('should skip weekends when getting previous trading day', () => {
|
||||
// Monday -> Friday
|
||||
const monday = new Date(2025, 5, 9);
|
||||
const friday = new Date(2025, 5, 6);
|
||||
expect(dateUtils.getPreviousTradingDay(monday).toDateString()).toBe(friday.toDateString());
|
||||
});
|
||||
|
||||
it('should handle weekends as input correctly', () => {
|
||||
// Saturday -> Friday
|
||||
const saturday = new Date(2025, 5, 7);
|
||||
const friday = new Date(2025, 5, 6);
|
||||
expect(dateUtils.getPreviousTradingDay(saturday).toDateString()).toBe(friday.toDateString());
|
||||
|
||||
// Sunday -> Friday
|
||||
const sunday = new Date(2025, 5, 8);
|
||||
expect(dateUtils.getPreviousTradingDay(sunday).toDateString()).toBe(friday.toDateString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue