removed old tests, created new ones and format
This commit is contained in:
parent
7579afa3c3
commit
b03231b849
57 changed files with 4092 additions and 5901 deletions
212
libs/utils/src/utils.test.ts
Normal file
212
libs/utils/src/utils.test.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
// Common utilities
|
||||
createProxyUrl,
|
||||
sleep,
|
||||
|
||||
// Date utilities
|
||||
dateUtils,
|
||||
|
||||
// Generic functions
|
||||
extractCloses,
|
||||
extractOHLC,
|
||||
extractVolumes,
|
||||
calculateSMA,
|
||||
calculateTypicalPrice,
|
||||
calculateTrueRange,
|
||||
calculateReturns,
|
||||
calculateLogReturns,
|
||||
calculateVWAP,
|
||||
filterBySymbol,
|
||||
filterByTimeRange,
|
||||
groupBySymbol,
|
||||
convertTimestamps,
|
||||
|
||||
} from './index';
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('common utilities', () => {
|
||||
it('should create proxy URL with auth', () => {
|
||||
const proxy = {
|
||||
protocol: 'http',
|
||||
host: '192.168.1.1',
|
||||
port: 8080,
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
};
|
||||
|
||||
const url = createProxyUrl(proxy);
|
||||
expect(url).toBe('http://user:pass@192.168.1.1:8080');
|
||||
});
|
||||
|
||||
it('should create proxy URL without auth', () => {
|
||||
const proxy = {
|
||||
protocol: 'socks5',
|
||||
host: '192.168.1.1',
|
||||
port: 1080,
|
||||
};
|
||||
|
||||
const url = createProxyUrl(proxy);
|
||||
expect(url).toBe('socks5://192.168.1.1:1080');
|
||||
});
|
||||
|
||||
it('should sleep for specified milliseconds', async () => {
|
||||
const start = Date.now();
|
||||
await sleep(100);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(elapsed).toBeGreaterThanOrEqual(90);
|
||||
expect(elapsed).toBeLessThan(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('date utilities', () => {
|
||||
it('should check if date is trading day', () => {
|
||||
const monday = new Date('2023-12-25'); // Monday
|
||||
const saturday = new Date('2023-12-23'); // Saturday
|
||||
const sunday = new Date('2023-12-24'); // Sunday
|
||||
|
||||
expect(dateUtils.isTradingDay(monday)).toBe(true);
|
||||
expect(dateUtils.isTradingDay(saturday)).toBe(false);
|
||||
expect(dateUtils.isTradingDay(sunday)).toBe(false);
|
||||
});
|
||||
|
||||
it('should get next trading day', () => {
|
||||
const friday = new Date('2023-12-22'); // Friday
|
||||
const nextDay = dateUtils.getNextTradingDay(friday);
|
||||
|
||||
expect(nextDay.getDay()).toBe(1); // Monday
|
||||
});
|
||||
|
||||
it('should get previous trading day', () => {
|
||||
const monday = new Date('2023-12-25'); // Monday
|
||||
const prevDay = dateUtils.getPreviousTradingDay(monday);
|
||||
|
||||
expect(prevDay.getDay()).toBe(5); // Friday
|
||||
});
|
||||
|
||||
it('should format date as YYYY-MM-DD', () => {
|
||||
const date = new Date('2023-12-25T10:30:00Z');
|
||||
const formatted = dateUtils.formatDate(date);
|
||||
|
||||
expect(formatted).toBe('2023-12-25');
|
||||
});
|
||||
|
||||
it('should parse date from string', () => {
|
||||
const date = dateUtils.parseDate('2023-12-25');
|
||||
|
||||
expect(date.getFullYear()).toBe(2023);
|
||||
expect(date.getMonth()).toBe(11); // 0-based
|
||||
expect(date.getDate()).toBe(25);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic functions', () => {
|
||||
const testData = [
|
||||
{ open: 100, high: 105, low: 98, close: 103, volume: 1000 },
|
||||
{ open: 103, high: 107, low: 101, close: 105, volume: 1200 },
|
||||
{ open: 105, high: 108, low: 104, close: 106, volume: 1100 },
|
||||
];
|
||||
|
||||
it('should extract close prices', () => {
|
||||
const closes = extractCloses(testData);
|
||||
expect(closes).toEqual([103, 105, 106]);
|
||||
});
|
||||
|
||||
it('should extract OHLC data', () => {
|
||||
const ohlc = extractOHLC(testData);
|
||||
|
||||
expect(ohlc.opens).toEqual([100, 103, 105]);
|
||||
expect(ohlc.highs).toEqual([105, 107, 108]);
|
||||
expect(ohlc.lows).toEqual([98, 101, 104]);
|
||||
expect(ohlc.closes).toEqual([103, 105, 106]);
|
||||
});
|
||||
|
||||
it('should extract volumes', () => {
|
||||
const volumes = extractVolumes(testData);
|
||||
expect(volumes).toEqual([1000, 1200, 1100]);
|
||||
});
|
||||
|
||||
it('should calculate SMA', () => {
|
||||
const sma = calculateSMA(testData, 2);
|
||||
expect(sma).toHaveLength(2);
|
||||
expect(sma[0]).toBe(104);
|
||||
expect(sma[1]).toBe(105.5);
|
||||
});
|
||||
|
||||
it('should calculate typical price', () => {
|
||||
const typical = calculateTypicalPrice(testData);
|
||||
|
||||
expect(typical[0]).toBeCloseTo((105 + 98 + 103) / 3);
|
||||
expect(typical[1]).toBeCloseTo((107 + 101 + 105) / 3);
|
||||
expect(typical[2]).toBeCloseTo((108 + 104 + 106) / 3);
|
||||
});
|
||||
|
||||
it('should calculate true range', () => {
|
||||
const tr = calculateTrueRange(testData);
|
||||
|
||||
expect(tr).toHaveLength(3);
|
||||
expect(tr[0]).toBe(7); // 105 - 98
|
||||
});
|
||||
|
||||
it('should calculate returns', () => {
|
||||
const returns = calculateReturns(testData);
|
||||
|
||||
expect(returns).toHaveLength(2);
|
||||
expect(returns[0]).toBeCloseTo((105 - 103) / 103);
|
||||
expect(returns[1]).toBeCloseTo((106 - 105) / 105);
|
||||
});
|
||||
|
||||
it('should calculate log returns', () => {
|
||||
const logReturns = calculateLogReturns(testData);
|
||||
|
||||
expect(logReturns).toHaveLength(2);
|
||||
expect(logReturns[0]).toBeCloseTo(Math.log(105 / 103));
|
||||
expect(logReturns[1]).toBeCloseTo(Math.log(106 / 105));
|
||||
});
|
||||
|
||||
it('should calculate VWAP', () => {
|
||||
const vwap = calculateVWAP(testData);
|
||||
|
||||
expect(vwap).toHaveLength(3);
|
||||
expect(vwap[0]).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OHLCV data operations', () => {
|
||||
const ohlcvData = [
|
||||
{ symbol: 'AAPL', open: 100, high: 105, low: 98, close: 103, volume: 1000, timestamp: 1000000 },
|
||||
{ symbol: 'GOOGL', open: 200, high: 205, low: 198, close: 203, volume: 2000, timestamp: 1000000 },
|
||||
{ symbol: 'AAPL', open: 103, high: 107, low: 101, close: 105, volume: 1200, timestamp: 2000000 },
|
||||
];
|
||||
|
||||
it('should filter by symbol', () => {
|
||||
const filtered = filterBySymbol(ohlcvData, 'AAPL');
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.every(item => item.symbol === 'AAPL')).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by time range', () => {
|
||||
const filtered = filterByTimeRange(ohlcvData, 1500000, 2500000);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].timestamp).toBe(2000000);
|
||||
});
|
||||
|
||||
it('should group by symbol', () => {
|
||||
const grouped = groupBySymbol(ohlcvData);
|
||||
|
||||
expect(grouped['AAPL']).toHaveLength(2);
|
||||
expect(grouped['GOOGL']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should convert timestamps to dates', () => {
|
||||
const converted = convertTimestamps(ohlcvData);
|
||||
|
||||
expect(converted[0].date).toBeInstanceOf(Date);
|
||||
expect(converted[0].date.getTime()).toBe(1000000);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,397 +0,0 @@
|
|||
/**
|
||||
* Test suite for position sizing calculations
|
||||
*/
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
atrBasedPositionSize,
|
||||
calculatePortfolioHeat,
|
||||
correlationAdjustedPositionSize,
|
||||
dynamicPositionSize,
|
||||
equalWeightPositionSize,
|
||||
expectancyPositionSize,
|
||||
fixedRiskPositionSize,
|
||||
kellyPositionSize,
|
||||
liquidityConstrainedPositionSize,
|
||||
multiTimeframePositionSize,
|
||||
riskParityPositionSize,
|
||||
sharpeOptimizedPositionSize,
|
||||
validatePositionSize,
|
||||
volatilityTargetPositionSize,
|
||||
type KellyParams,
|
||||
type PositionSizeParams,
|
||||
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.2,
|
||||
targetVolatility: 0.1,
|
||||
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.2,
|
||||
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.1);
|
||||
|
||||
// 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.2, 0.15, 0.15, 0.1);
|
||||
|
||||
// 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.1, price: 100 },
|
||||
{ volatility: 0.2, 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.2, 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.2, 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.2, 0.02)).toBe(0);
|
||||
|
||||
// Expected return less than risk-free rate
|
||||
expect(sharpeOptimizedPositionSize(100000, 0.01, 0.2, 0.02)).toBe(0);
|
||||
});
|
||||
|
||||
it('should respect maximum leverage', () => {
|
||||
const result = sharpeOptimizedPositionSize(100000, 0.3, 0.2, 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 +0,0 @@
|
|||
import { describe, expect, it } 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