linxus fs fixes
This commit is contained in:
parent
ac23b70146
commit
0b7846fe67
292 changed files with 41947 additions and 41947 deletions
|
|
@ -1,97 +1,97 @@
|
|||
# Position Sizing Calculations - Fixed Issues Summary
|
||||
|
||||
## Issues Identified and Fixed:
|
||||
|
||||
### 1. **Duplicate Kelly Function** ✅ FIXED
|
||||
- **Problem**: Two different `kellyPositionSize` functions with conflicting signatures
|
||||
- **Solution**: Removed the duplicate and kept the version with proper `KellyParams` interface
|
||||
|
||||
### 2. **Incorrect Kelly Criterion Formula** ✅ FIXED
|
||||
- **Problem**: Formula was implemented as `winRate - ((1 - winRate) / winLossRatio)`
|
||||
- **Solution**: Corrected to `(winRate * winLossRatio - lossRate) / winLossRatio`
|
||||
- **Mathematical Validation**: Kelly formula is `f = (bp - q) / b` where b = win/loss ratio, p = win rate, q = loss rate
|
||||
|
||||
### 3. **Missing Input Validation** ✅ FIXED
|
||||
- **Problem**: Functions didn't validate inputs (zero/negative values)
|
||||
- **Solution**: Added comprehensive input validation to all functions
|
||||
- **Examples**:
|
||||
- Check for `accountSize <= 0`, `riskPercentage <= 0`
|
||||
- Validate `winRate` is between 0 and 1
|
||||
- Ensure prices and volatilities are positive
|
||||
|
||||
### 4. **ATR Position Sizing Units Error** ✅ FIXED
|
||||
- **Problem**: Function returned risk amount instead of shares
|
||||
- **Solution**: Changed to return `Math.floor(riskAmount / stopDistance)` (shares)
|
||||
|
||||
### 5. **Flawed Monte Carlo Simulation** ✅ FIXED
|
||||
- **Problem**: Simulation applied returns to entire portfolio instead of position-sized returns
|
||||
- **Solution**: Rewritten to test different position fractions and optimize based on Sharpe ratio
|
||||
|
||||
### 6. **Redundant Liquidity Calculations** ✅ FIXED
|
||||
- **Problem**: Unnecessary conversions between shares and dollar values
|
||||
- **Solution**: Simplified to directly compare `desiredPositionSize` with `maxShares`
|
||||
|
||||
### 7. **Risk Parity Not Using Target Risk** ✅ FIXED
|
||||
- **Problem**: `targetRisk` parameter was ignored in calculations
|
||||
- **Solution**: Incorporated target risk into weight calculations: `weight * (targetRisk / asset.volatility)`
|
||||
|
||||
### 8. **Missing Safety Constraints** ✅ FIXED
|
||||
- **Problem**: No caps on leverage or volatility ratios
|
||||
- **Solution**: Added reasonable caps:
|
||||
- Volatility targeting: max 2x leverage
|
||||
- Volatility adjustment: max 3x leverage
|
||||
- Kelly fraction: max 25% with safety factor
|
||||
|
||||
### 9. **Correlation Risk Calculation Error** ✅ FIXED
|
||||
- **Problem**: Correlation risk calculation didn't consider relative position sizes
|
||||
- **Solution**: Weight correlations by relative position sizes for more accurate risk assessment
|
||||
|
||||
### 10. **Integer Share Handling** ✅ FIXED
|
||||
- **Problem**: Functions returned fractional shares
|
||||
- **Solution**: Added `Math.floor()` to return whole shares where appropriate
|
||||
|
||||
## Mathematical Validation Examples:
|
||||
|
||||
### Fixed Risk Position Sizing:
|
||||
```
|
||||
Account: $100,000, Risk: 2%, Entry: $100, Stop: $95
|
||||
Risk Amount: $100,000 × 0.02 = $2,000
|
||||
Risk Per Share: |$100 - $95| = $5
|
||||
Position Size: $2,000 ÷ $5 = 400 shares ✅
|
||||
```
|
||||
|
||||
### Kelly Criterion (Corrected):
|
||||
```
|
||||
Win Rate: 60%, Avg Win: $150, Avg Loss: $100
|
||||
Win/Loss Ratio: $150 ÷ $100 = 1.5
|
||||
Kelly Fraction: (1.5 × 0.6 - 0.4) ÷ 1.5 = 0.333
|
||||
With Safety Factor (25%): 0.333 × 0.25 = 0.083
|
||||
Position: $100,000 × 0.083 = $8,333 ✅
|
||||
```
|
||||
|
||||
### Volatility Targeting:
|
||||
```
|
||||
Price: $100, Asset Vol: 20%, Target Vol: 10%
|
||||
Volatility Ratio: 10% ÷ 20% = 0.5
|
||||
Position Value: $100,000 × 0.5 = $50,000
|
||||
Position Size: $50,000 ÷ $100 = 500 shares ✅
|
||||
```
|
||||
|
||||
## Edge Cases Now Handled:
|
||||
- ✅ Zero or negative account sizes
|
||||
- ✅ Equal entry and stop loss prices
|
||||
- ✅ Zero volatility assets
|
||||
- ✅ Negative expectancy strategies
|
||||
- ✅ Extreme correlation values
|
||||
- ✅ Division by zero scenarios
|
||||
- ✅ Invalid win rates (≤0 or ≥1)
|
||||
|
||||
## Additional Improvements:
|
||||
- ✅ Consistent return types (whole shares vs. dollar amounts)
|
||||
- ✅ Proper TypeScript interfaces for all parameters
|
||||
- ✅ Comprehensive JSDoc documentation
|
||||
- ✅ Mathematical formulas verified against financial literature
|
||||
- ✅ Safety factors to prevent over-leveraging
|
||||
- ✅ Portfolio-level risk management functions
|
||||
|
||||
All position sizing calculations are now mathematically correct, properly validated, and production-ready!
|
||||
# Position Sizing Calculations - Fixed Issues Summary
|
||||
|
||||
## Issues Identified and Fixed:
|
||||
|
||||
### 1. **Duplicate Kelly Function** ✅ FIXED
|
||||
- **Problem**: Two different `kellyPositionSize` functions with conflicting signatures
|
||||
- **Solution**: Removed the duplicate and kept the version with proper `KellyParams` interface
|
||||
|
||||
### 2. **Incorrect Kelly Criterion Formula** ✅ FIXED
|
||||
- **Problem**: Formula was implemented as `winRate - ((1 - winRate) / winLossRatio)`
|
||||
- **Solution**: Corrected to `(winRate * winLossRatio - lossRate) / winLossRatio`
|
||||
- **Mathematical Validation**: Kelly formula is `f = (bp - q) / b` where b = win/loss ratio, p = win rate, q = loss rate
|
||||
|
||||
### 3. **Missing Input Validation** ✅ FIXED
|
||||
- **Problem**: Functions didn't validate inputs (zero/negative values)
|
||||
- **Solution**: Added comprehensive input validation to all functions
|
||||
- **Examples**:
|
||||
- Check for `accountSize <= 0`, `riskPercentage <= 0`
|
||||
- Validate `winRate` is between 0 and 1
|
||||
- Ensure prices and volatilities are positive
|
||||
|
||||
### 4. **ATR Position Sizing Units Error** ✅ FIXED
|
||||
- **Problem**: Function returned risk amount instead of shares
|
||||
- **Solution**: Changed to return `Math.floor(riskAmount / stopDistance)` (shares)
|
||||
|
||||
### 5. **Flawed Monte Carlo Simulation** ✅ FIXED
|
||||
- **Problem**: Simulation applied returns to entire portfolio instead of position-sized returns
|
||||
- **Solution**: Rewritten to test different position fractions and optimize based on Sharpe ratio
|
||||
|
||||
### 6. **Redundant Liquidity Calculations** ✅ FIXED
|
||||
- **Problem**: Unnecessary conversions between shares and dollar values
|
||||
- **Solution**: Simplified to directly compare `desiredPositionSize` with `maxShares`
|
||||
|
||||
### 7. **Risk Parity Not Using Target Risk** ✅ FIXED
|
||||
- **Problem**: `targetRisk` parameter was ignored in calculations
|
||||
- **Solution**: Incorporated target risk into weight calculations: `weight * (targetRisk / asset.volatility)`
|
||||
|
||||
### 8. **Missing Safety Constraints** ✅ FIXED
|
||||
- **Problem**: No caps on leverage or volatility ratios
|
||||
- **Solution**: Added reasonable caps:
|
||||
- Volatility targeting: max 2x leverage
|
||||
- Volatility adjustment: max 3x leverage
|
||||
- Kelly fraction: max 25% with safety factor
|
||||
|
||||
### 9. **Correlation Risk Calculation Error** ✅ FIXED
|
||||
- **Problem**: Correlation risk calculation didn't consider relative position sizes
|
||||
- **Solution**: Weight correlations by relative position sizes for more accurate risk assessment
|
||||
|
||||
### 10. **Integer Share Handling** ✅ FIXED
|
||||
- **Problem**: Functions returned fractional shares
|
||||
- **Solution**: Added `Math.floor()` to return whole shares where appropriate
|
||||
|
||||
## Mathematical Validation Examples:
|
||||
|
||||
### Fixed Risk Position Sizing:
|
||||
```
|
||||
Account: $100,000, Risk: 2%, Entry: $100, Stop: $95
|
||||
Risk Amount: $100,000 × 0.02 = $2,000
|
||||
Risk Per Share: |$100 - $95| = $5
|
||||
Position Size: $2,000 ÷ $5 = 400 shares ✅
|
||||
```
|
||||
|
||||
### Kelly Criterion (Corrected):
|
||||
```
|
||||
Win Rate: 60%, Avg Win: $150, Avg Loss: $100
|
||||
Win/Loss Ratio: $150 ÷ $100 = 1.5
|
||||
Kelly Fraction: (1.5 × 0.6 - 0.4) ÷ 1.5 = 0.333
|
||||
With Safety Factor (25%): 0.333 × 0.25 = 0.083
|
||||
Position: $100,000 × 0.083 = $8,333 ✅
|
||||
```
|
||||
|
||||
### Volatility Targeting:
|
||||
```
|
||||
Price: $100, Asset Vol: 20%, Target Vol: 10%
|
||||
Volatility Ratio: 10% ÷ 20% = 0.5
|
||||
Position Value: $100,000 × 0.5 = $50,000
|
||||
Position Size: $50,000 ÷ $100 = 500 shares ✅
|
||||
```
|
||||
|
||||
## Edge Cases Now Handled:
|
||||
- ✅ Zero or negative account sizes
|
||||
- ✅ Equal entry and stop loss prices
|
||||
- ✅ Zero volatility assets
|
||||
- ✅ Negative expectancy strategies
|
||||
- ✅ Extreme correlation values
|
||||
- ✅ Division by zero scenarios
|
||||
- ✅ Invalid win rates (≤0 or ≥1)
|
||||
|
||||
## Additional Improvements:
|
||||
- ✅ Consistent return types (whole shares vs. dollar amounts)
|
||||
- ✅ Proper TypeScript interfaces for all parameters
|
||||
- ✅ Comprehensive JSDoc documentation
|
||||
- ✅ Mathematical formulas verified against financial literature
|
||||
- ✅ Safety factors to prevent over-leveraging
|
||||
- ✅ Portfolio-level risk management functions
|
||||
|
||||
All position sizing calculations are now mathematically correct, properly validated, and production-ready!
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
# Utils Library
|
||||
|
||||
Common utility functions shared across services in the stock-bot project.
|
||||
|
||||
## Included Utilities
|
||||
|
||||
### Date Utilities
|
||||
|
||||
Helper functions for working with market dates:
|
||||
|
||||
```typescript
|
||||
import { dateUtils } from '@stock-bot/utils';
|
||||
|
||||
// Check if a date is a trading day
|
||||
const isTradingDay = dateUtils.isTradingDay(new Date());
|
||||
|
||||
// Get the next trading day
|
||||
const nextTradingDay = dateUtils.getNextTradingDay(new Date());
|
||||
```
|
||||
|
||||
### Financial Utilities
|
||||
|
||||
Mathematical functions for financial calculations:
|
||||
|
||||
```typescript
|
||||
import { calculateCAGR, calculateSharpeRatio } from '@stock-bot/utils';
|
||||
|
||||
// Calculate compound annual growth rate
|
||||
const returns = [0.05, 0.03, -0.01, 0.04, 0.02];
|
||||
const cagr = calculateCAGR(startValue, endValue, years);
|
||||
|
||||
// Calculate Sharpe ratio
|
||||
const sharpeRatio = calculateSharpeRatio(returns, 0.02);
|
||||
```
|
||||
# Utils Library
|
||||
|
||||
Common utility functions shared across services in the stock-bot project.
|
||||
|
||||
## Included Utilities
|
||||
|
||||
### Date Utilities
|
||||
|
||||
Helper functions for working with market dates:
|
||||
|
||||
```typescript
|
||||
import { dateUtils } from '@stock-bot/utils';
|
||||
|
||||
// Check if a date is a trading day
|
||||
const isTradingDay = dateUtils.isTradingDay(new Date());
|
||||
|
||||
// Get the next trading day
|
||||
const nextTradingDay = dateUtils.getNextTradingDay(new Date());
|
||||
```
|
||||
|
||||
### Financial Utilities
|
||||
|
||||
Mathematical functions for financial calculations:
|
||||
|
||||
```typescript
|
||||
import { calculateCAGR, calculateSharpeRatio } from '@stock-bot/utils';
|
||||
|
||||
// Calculate compound annual growth rate
|
||||
const returns = [0.05, 0.03, -0.01, 0.04, 0.02];
|
||||
const cagr = calculateCAGR(startValue, endValue, years);
|
||||
|
||||
// Calculate Sharpe ratio
|
||||
const sharpeRatio = calculateSharpeRatio(returns, 0.02);
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
{
|
||||
"name": "@stock-bot/utils",
|
||||
"version": "1.0.0",
|
||||
"description": "Common utility functions for stock-bot services",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rimraf dist",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/types": "*",
|
||||
"date-fns": "^2.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.0",
|
||||
"bun-types": "^1.2.15"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "@stock-bot/utils",
|
||||
"version": "1.0.0",
|
||||
"description": "Common utility functions for stock-bot services",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rimraf dist",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stock-bot/types": "*",
|
||||
"date-fns": "^2.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.0",
|
||||
"bun-types": "^1.2.15"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,391 +1,391 @@
|
|||
/**
|
||||
* Basic Financial Calculations
|
||||
* Core mathematical functions for financial analysis
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate percentage change between two values
|
||||
*/
|
||||
export function percentageChange(oldValue: number, newValue: number): number {
|
||||
if (oldValue === 0) return 0;
|
||||
return ((newValue - oldValue) / oldValue) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate simple return
|
||||
*/
|
||||
export function simpleReturn(initialPrice: number, finalPrice: number): number {
|
||||
if (initialPrice === 0) return 0;
|
||||
return (finalPrice - initialPrice) / initialPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate logarithmic return
|
||||
*/
|
||||
export function logReturn(initialPrice: number, finalPrice: number): number {
|
||||
if (initialPrice <= 0 || finalPrice <= 0) return 0;
|
||||
return Math.log(finalPrice / initialPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compound annual growth rate (CAGR)
|
||||
*/
|
||||
export function cagr(startValue: number, endValue: number, years: number): number {
|
||||
if (years <= 0 || startValue <= 0 || endValue <= 0) return 0;
|
||||
return Math.pow(endValue / startValue, 1 / years) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate annualized return from periodic returns
|
||||
*/
|
||||
export function annualizeReturn(periodicReturn: number, periodsPerYear: number): number {
|
||||
return Math.pow(1 + periodicReturn, periodsPerYear) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate annualized volatility from periodic returns
|
||||
*/
|
||||
export function annualizeVolatility(periodicVolatility: number, periodsPerYear: number): number {
|
||||
return periodicVolatility * Math.sqrt(periodsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate present value
|
||||
*/
|
||||
export function presentValue(futureValue: number, rate: number, periods: number): number {
|
||||
return futureValue / Math.pow(1 + rate, periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate future value
|
||||
*/
|
||||
export function futureValue(presentValue: number, rate: number, periods: number): number {
|
||||
return presentValue * Math.pow(1 + rate, periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate net present value of cash flows
|
||||
*/
|
||||
export function netPresentValue(cashFlows: number[], discountRate: number): number {
|
||||
return cashFlows.reduce((npv, cashFlow, index) => {
|
||||
return npv + cashFlow / Math.pow(1 + discountRate, index);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate internal rate of return (IRR) using Newton-Raphson method
|
||||
*/
|
||||
export function internalRateOfReturn(cashFlows: number[], guess: number = 0.1, maxIterations: number = 100): number {
|
||||
let rate = guess;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
let npv = 0;
|
||||
let dnpv = 0;
|
||||
|
||||
for (let j = 0; j < cashFlows.length; j++) {
|
||||
npv += cashFlows[j] / Math.pow(1 + rate, j);
|
||||
dnpv += -j * cashFlows[j] / Math.pow(1 + rate, j + 1);
|
||||
}
|
||||
|
||||
if (Math.abs(npv) < 1e-10) break;
|
||||
if (Math.abs(dnpv) < 1e-10) break;
|
||||
|
||||
rate = rate - npv / dnpv;
|
||||
}
|
||||
|
||||
return rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate payback period
|
||||
*/
|
||||
export function paybackPeriod(initialInvestment: number, cashFlows: number[]): number {
|
||||
let cumulativeCashFlow = 0;
|
||||
|
||||
for (let i = 0; i < cashFlows.length; i++) {
|
||||
cumulativeCashFlow += cashFlows[i];
|
||||
if (cumulativeCashFlow >= initialInvestment) {
|
||||
return i + 1 - (cumulativeCashFlow - initialInvestment) / cashFlows[i];
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // Never pays back
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compound interest
|
||||
*/
|
||||
export function compoundInterest(
|
||||
principal: number,
|
||||
rate: number,
|
||||
periods: number,
|
||||
compoundingFrequency: number = 1
|
||||
): number {
|
||||
return principal * Math.pow(1 + rate / compoundingFrequency, compoundingFrequency * periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate effective annual rate
|
||||
*/
|
||||
export function effectiveAnnualRate(nominalRate: number, compoundingFrequency: number): number {
|
||||
return Math.pow(1 + nominalRate / compoundingFrequency, compoundingFrequency) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond price given yield
|
||||
*/
|
||||
export function bondPrice(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
|
||||
let price = 0;
|
||||
|
||||
// Present value of coupon payments
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
price += couponPayment / Math.pow(1 + discountRate, i);
|
||||
}
|
||||
|
||||
// Present value of face value
|
||||
price += faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond yield given price (Newton-Raphson approximation)
|
||||
*/
|
||||
export function bondYield(
|
||||
price: number,
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2,
|
||||
guess: number = 0.05
|
||||
): number {
|
||||
let yield_ = guess;
|
||||
const maxIterations = 100;
|
||||
const tolerance = 1e-8;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
const calculatedPrice = bondPrice(faceValue, couponRate, yield_, periodsToMaturity, paymentsPerYear);
|
||||
const diff = calculatedPrice - price;
|
||||
|
||||
if (Math.abs(diff) < tolerance) break;
|
||||
|
||||
// Numerical derivative
|
||||
const delta = 0.0001;
|
||||
const priceUp = bondPrice(faceValue, couponRate, yield_ + delta, periodsToMaturity, paymentsPerYear);
|
||||
const derivative = (priceUp - calculatedPrice) / delta;
|
||||
|
||||
if (Math.abs(derivative) < tolerance) break;
|
||||
|
||||
yield_ = yield_ - diff / derivative;
|
||||
}
|
||||
|
||||
return yield_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration (Macaulay duration)
|
||||
*/
|
||||
export function macaulayDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
|
||||
let weightedTime = 0;
|
||||
|
||||
// Weighted time of coupon payments
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
const presentValue = couponPayment / Math.pow(1 + discountRate, i);
|
||||
weightedTime += (i * presentValue) / bondPriceValue;
|
||||
}
|
||||
|
||||
// Weighted time of face value
|
||||
const faceValuePV = faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
weightedTime += (periodsToMaturity * faceValuePV) / bondPriceValue;
|
||||
|
||||
return weightedTime / paymentsPerYear; // Convert to years
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate modified duration
|
||||
*/
|
||||
export function modifiedDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const macDuration = macaulayDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
return macDuration / (1 + yieldToMaturity / paymentsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond convexity
|
||||
*/
|
||||
export function bondConvexity(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
|
||||
let convexity = 0;
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
const presentValue = couponPayment / Math.pow(1 + discountRate, i);
|
||||
convexity += (i * (i + 1) * presentValue) / Math.pow(1 + discountRate, 2);
|
||||
}
|
||||
|
||||
const faceValuePV = faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
convexity += (periodsToMaturity * (periodsToMaturity + 1) * faceValuePV) / Math.pow(1 + discountRate, 2);
|
||||
|
||||
return convexity / (bondPriceValue * paymentsPerYear * paymentsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dollar duration
|
||||
*/
|
||||
export function dollarDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2,
|
||||
basisPointChange: number = 0.01 // 1 basis point = 0.01%
|
||||
): number {
|
||||
const modifiedDur = modifiedDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
return modifiedDur * bondPriceValue * basisPointChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate accrued interest
|
||||
*/
|
||||
export function accruedInterest(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
daysSinceLastCoupon: number,
|
||||
daysInCouponPeriod: number
|
||||
): number {
|
||||
return (faceValue * couponRate) * (daysSinceLastCoupon / daysInCouponPeriod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate clean price
|
||||
*/
|
||||
export function cleanPrice(dirtyPrice: number, accruedInterestValue: number): number {
|
||||
return dirtyPrice - accruedInterestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dirty price
|
||||
*/
|
||||
export function dirtyPrice(cleanPriceValue: number, accruedInterestValue: number): number {
|
||||
return cleanPriceValue + accruedInterestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dividend discount model (DDM)
|
||||
*/
|
||||
export function dividendDiscountModel(
|
||||
currentDividend: number,
|
||||
growthRate: number,
|
||||
discountRate: number
|
||||
): number {
|
||||
if (discountRate <= growthRate) return NaN; // Indeterminate
|
||||
return currentDividend * (1 + growthRate) / (discountRate - growthRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weighted average cost of capital (WACC)
|
||||
*/
|
||||
export function weightedAverageCostOfCapital(
|
||||
costOfEquity: number,
|
||||
costOfDebt: number,
|
||||
equityWeight: number,
|
||||
debtWeight: number,
|
||||
taxRate: number
|
||||
): number {
|
||||
return (equityWeight * costOfEquity) + (debtWeight * costOfDebt * (1 - taxRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate capital asset pricing model (CAPM)
|
||||
*/
|
||||
export function capitalAssetPricingModel(
|
||||
riskFreeRate: number,
|
||||
beta: number,
|
||||
marketRiskPremium: number
|
||||
): number {
|
||||
return riskFreeRate + beta * marketRiskPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate hurdle rate
|
||||
*/
|
||||
export function hurdleRate(
|
||||
costOfCapital: number,
|
||||
riskPremium: number
|
||||
): number {
|
||||
return costOfCapital + riskPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of operating leverage (DOL)
|
||||
*/
|
||||
export function degreeOfOperatingLeverage(
|
||||
contributionMargin: number,
|
||||
operatingIncome: number
|
||||
): number {
|
||||
return contributionMargin / operatingIncome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of financial leverage (DFL)
|
||||
*/
|
||||
export function degreeOfFinancialLeverage(
|
||||
ebit: number,
|
||||
earningsBeforeTax: number
|
||||
): number {
|
||||
return ebit / earningsBeforeTax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of total leverage (DTL)
|
||||
*/
|
||||
export function degreeOfTotalLeverage(
|
||||
dol: number,
|
||||
dfl: number
|
||||
): number {
|
||||
return dol * dfl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate economic value added (EVA)
|
||||
*/
|
||||
export function economicValueAdded(
|
||||
netOperatingProfitAfterTax: number,
|
||||
capitalInvested: number,
|
||||
wacc: number
|
||||
): number {
|
||||
return netOperatingProfitAfterTax - (capitalInvested * wacc);
|
||||
}
|
||||
/**
|
||||
* Basic Financial Calculations
|
||||
* Core mathematical functions for financial analysis
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate percentage change between two values
|
||||
*/
|
||||
export function percentageChange(oldValue: number, newValue: number): number {
|
||||
if (oldValue === 0) return 0;
|
||||
return ((newValue - oldValue) / oldValue) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate simple return
|
||||
*/
|
||||
export function simpleReturn(initialPrice: number, finalPrice: number): number {
|
||||
if (initialPrice === 0) return 0;
|
||||
return (finalPrice - initialPrice) / initialPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate logarithmic return
|
||||
*/
|
||||
export function logReturn(initialPrice: number, finalPrice: number): number {
|
||||
if (initialPrice <= 0 || finalPrice <= 0) return 0;
|
||||
return Math.log(finalPrice / initialPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compound annual growth rate (CAGR)
|
||||
*/
|
||||
export function cagr(startValue: number, endValue: number, years: number): number {
|
||||
if (years <= 0 || startValue <= 0 || endValue <= 0) return 0;
|
||||
return Math.pow(endValue / startValue, 1 / years) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate annualized return from periodic returns
|
||||
*/
|
||||
export function annualizeReturn(periodicReturn: number, periodsPerYear: number): number {
|
||||
return Math.pow(1 + periodicReturn, periodsPerYear) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate annualized volatility from periodic returns
|
||||
*/
|
||||
export function annualizeVolatility(periodicVolatility: number, periodsPerYear: number): number {
|
||||
return periodicVolatility * Math.sqrt(periodsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate present value
|
||||
*/
|
||||
export function presentValue(futureValue: number, rate: number, periods: number): number {
|
||||
return futureValue / Math.pow(1 + rate, periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate future value
|
||||
*/
|
||||
export function futureValue(presentValue: number, rate: number, periods: number): number {
|
||||
return presentValue * Math.pow(1 + rate, periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate net present value of cash flows
|
||||
*/
|
||||
export function netPresentValue(cashFlows: number[], discountRate: number): number {
|
||||
return cashFlows.reduce((npv, cashFlow, index) => {
|
||||
return npv + cashFlow / Math.pow(1 + discountRate, index);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate internal rate of return (IRR) using Newton-Raphson method
|
||||
*/
|
||||
export function internalRateOfReturn(cashFlows: number[], guess: number = 0.1, maxIterations: number = 100): number {
|
||||
let rate = guess;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
let npv = 0;
|
||||
let dnpv = 0;
|
||||
|
||||
for (let j = 0; j < cashFlows.length; j++) {
|
||||
npv += cashFlows[j] / Math.pow(1 + rate, j);
|
||||
dnpv += -j * cashFlows[j] / Math.pow(1 + rate, j + 1);
|
||||
}
|
||||
|
||||
if (Math.abs(npv) < 1e-10) break;
|
||||
if (Math.abs(dnpv) < 1e-10) break;
|
||||
|
||||
rate = rate - npv / dnpv;
|
||||
}
|
||||
|
||||
return rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate payback period
|
||||
*/
|
||||
export function paybackPeriod(initialInvestment: number, cashFlows: number[]): number {
|
||||
let cumulativeCashFlow = 0;
|
||||
|
||||
for (let i = 0; i < cashFlows.length; i++) {
|
||||
cumulativeCashFlow += cashFlows[i];
|
||||
if (cumulativeCashFlow >= initialInvestment) {
|
||||
return i + 1 - (cumulativeCashFlow - initialInvestment) / cashFlows[i];
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // Never pays back
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compound interest
|
||||
*/
|
||||
export function compoundInterest(
|
||||
principal: number,
|
||||
rate: number,
|
||||
periods: number,
|
||||
compoundingFrequency: number = 1
|
||||
): number {
|
||||
return principal * Math.pow(1 + rate / compoundingFrequency, compoundingFrequency * periods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate effective annual rate
|
||||
*/
|
||||
export function effectiveAnnualRate(nominalRate: number, compoundingFrequency: number): number {
|
||||
return Math.pow(1 + nominalRate / compoundingFrequency, compoundingFrequency) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond price given yield
|
||||
*/
|
||||
export function bondPrice(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
|
||||
let price = 0;
|
||||
|
||||
// Present value of coupon payments
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
price += couponPayment / Math.pow(1 + discountRate, i);
|
||||
}
|
||||
|
||||
// Present value of face value
|
||||
price += faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond yield given price (Newton-Raphson approximation)
|
||||
*/
|
||||
export function bondYield(
|
||||
price: number,
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2,
|
||||
guess: number = 0.05
|
||||
): number {
|
||||
let yield_ = guess;
|
||||
const maxIterations = 100;
|
||||
const tolerance = 1e-8;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
const calculatedPrice = bondPrice(faceValue, couponRate, yield_, periodsToMaturity, paymentsPerYear);
|
||||
const diff = calculatedPrice - price;
|
||||
|
||||
if (Math.abs(diff) < tolerance) break;
|
||||
|
||||
// Numerical derivative
|
||||
const delta = 0.0001;
|
||||
const priceUp = bondPrice(faceValue, couponRate, yield_ + delta, periodsToMaturity, paymentsPerYear);
|
||||
const derivative = (priceUp - calculatedPrice) / delta;
|
||||
|
||||
if (Math.abs(derivative) < tolerance) break;
|
||||
|
||||
yield_ = yield_ - diff / derivative;
|
||||
}
|
||||
|
||||
return yield_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration (Macaulay duration)
|
||||
*/
|
||||
export function macaulayDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
|
||||
let weightedTime = 0;
|
||||
|
||||
// Weighted time of coupon payments
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
const presentValue = couponPayment / Math.pow(1 + discountRate, i);
|
||||
weightedTime += (i * presentValue) / bondPriceValue;
|
||||
}
|
||||
|
||||
// Weighted time of face value
|
||||
const faceValuePV = faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
weightedTime += (periodsToMaturity * faceValuePV) / bondPriceValue;
|
||||
|
||||
return weightedTime / paymentsPerYear; // Convert to years
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate modified duration
|
||||
*/
|
||||
export function modifiedDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const macDuration = macaulayDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
return macDuration / (1 + yieldToMaturity / paymentsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bond convexity
|
||||
*/
|
||||
export function bondConvexity(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2
|
||||
): number {
|
||||
const couponPayment = (faceValue * couponRate) / paymentsPerYear;
|
||||
const discountRate = yieldToMaturity / paymentsPerYear;
|
||||
|
||||
let convexity = 0;
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
|
||||
for (let i = 1; i <= periodsToMaturity; i++) {
|
||||
const presentValue = couponPayment / Math.pow(1 + discountRate, i);
|
||||
convexity += (i * (i + 1) * presentValue) / Math.pow(1 + discountRate, 2);
|
||||
}
|
||||
|
||||
const faceValuePV = faceValue / Math.pow(1 + discountRate, periodsToMaturity);
|
||||
convexity += (periodsToMaturity * (periodsToMaturity + 1) * faceValuePV) / Math.pow(1 + discountRate, 2);
|
||||
|
||||
return convexity / (bondPriceValue * paymentsPerYear * paymentsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dollar duration
|
||||
*/
|
||||
export function dollarDuration(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
yieldToMaturity: number,
|
||||
periodsToMaturity: number,
|
||||
paymentsPerYear: number = 2,
|
||||
basisPointChange: number = 0.01 // 1 basis point = 0.01%
|
||||
): number {
|
||||
const modifiedDur = modifiedDuration(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
const bondPriceValue = bondPrice(faceValue, couponRate, yieldToMaturity, periodsToMaturity, paymentsPerYear);
|
||||
return modifiedDur * bondPriceValue * basisPointChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate accrued interest
|
||||
*/
|
||||
export function accruedInterest(
|
||||
faceValue: number,
|
||||
couponRate: number,
|
||||
daysSinceLastCoupon: number,
|
||||
daysInCouponPeriod: number
|
||||
): number {
|
||||
return (faceValue * couponRate) * (daysSinceLastCoupon / daysInCouponPeriod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate clean price
|
||||
*/
|
||||
export function cleanPrice(dirtyPrice: number, accruedInterestValue: number): number {
|
||||
return dirtyPrice - accruedInterestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dirty price
|
||||
*/
|
||||
export function dirtyPrice(cleanPriceValue: number, accruedInterestValue: number): number {
|
||||
return cleanPriceValue + accruedInterestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dividend discount model (DDM)
|
||||
*/
|
||||
export function dividendDiscountModel(
|
||||
currentDividend: number,
|
||||
growthRate: number,
|
||||
discountRate: number
|
||||
): number {
|
||||
if (discountRate <= growthRate) return NaN; // Indeterminate
|
||||
return currentDividend * (1 + growthRate) / (discountRate - growthRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weighted average cost of capital (WACC)
|
||||
*/
|
||||
export function weightedAverageCostOfCapital(
|
||||
costOfEquity: number,
|
||||
costOfDebt: number,
|
||||
equityWeight: number,
|
||||
debtWeight: number,
|
||||
taxRate: number
|
||||
): number {
|
||||
return (equityWeight * costOfEquity) + (debtWeight * costOfDebt * (1 - taxRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate capital asset pricing model (CAPM)
|
||||
*/
|
||||
export function capitalAssetPricingModel(
|
||||
riskFreeRate: number,
|
||||
beta: number,
|
||||
marketRiskPremium: number
|
||||
): number {
|
||||
return riskFreeRate + beta * marketRiskPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate hurdle rate
|
||||
*/
|
||||
export function hurdleRate(
|
||||
costOfCapital: number,
|
||||
riskPremium: number
|
||||
): number {
|
||||
return costOfCapital + riskPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of operating leverage (DOL)
|
||||
*/
|
||||
export function degreeOfOperatingLeverage(
|
||||
contributionMargin: number,
|
||||
operatingIncome: number
|
||||
): number {
|
||||
return contributionMargin / operatingIncome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of financial leverage (DFL)
|
||||
*/
|
||||
export function degreeOfFinancialLeverage(
|
||||
ebit: number,
|
||||
earningsBeforeTax: number
|
||||
): number {
|
||||
return ebit / earningsBeforeTax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate degree of total leverage (DTL)
|
||||
*/
|
||||
export function degreeOfTotalLeverage(
|
||||
dol: number,
|
||||
dfl: number
|
||||
): number {
|
||||
return dol * dfl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate economic value added (EVA)
|
||||
*/
|
||||
export function economicValueAdded(
|
||||
netOperatingProfitAfterTax: number,
|
||||
capitalInvested: number,
|
||||
wacc: number
|
||||
): number {
|
||||
return netOperatingProfitAfterTax - (capitalInvested * wacc);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,166 +1,166 @@
|
|||
/**
|
||||
* Comprehensive Financial Calculations Library
|
||||
*
|
||||
* This module provides a complete set of financial calculations for trading and investment analysis.
|
||||
* Organized into logical categories for easy use and maintenance.
|
||||
*/
|
||||
|
||||
// Core interfaces for financial data
|
||||
export interface OHLCVData {
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface PriceData {
|
||||
price: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Financial calculation result interfaces
|
||||
export interface PortfolioMetrics {
|
||||
totalValue: number;
|
||||
totalReturn: number;
|
||||
totalReturnPercent: number;
|
||||
dailyReturn: number;
|
||||
dailyReturnPercent: number;
|
||||
maxDrawdown: number;
|
||||
sharpeRatio: number;
|
||||
beta: number;
|
||||
alpha: number;
|
||||
volatility: number;
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
var95: number; // Value at Risk 95%
|
||||
var99: number; // Value at Risk 99%
|
||||
cvar95: number; // Conditional VaR 95%
|
||||
maxDrawdown: number;
|
||||
volatility: number;
|
||||
downside_deviation: number;
|
||||
calmar_ratio: number;
|
||||
sortino_ratio: number;
|
||||
beta: number;
|
||||
alpha: number;
|
||||
sharpeRatio: number;
|
||||
treynorRatio: number;
|
||||
trackingError: number;
|
||||
informationRatio: number;
|
||||
}
|
||||
|
||||
export interface TechnicalIndicators {
|
||||
sma: number[];
|
||||
ema: number[];
|
||||
rsi: number[];
|
||||
macd: { macd: number[], signal: number[], histogram: number[] };
|
||||
bollinger: { upper: number[], middle: number[], lower: number[] };
|
||||
atr: number[];
|
||||
stochastic: { k: number[], d: number[] };
|
||||
williams_r: number[];
|
||||
cci: number[];
|
||||
momentum: number[];
|
||||
roc: number[];
|
||||
}
|
||||
|
||||
// Additional interfaces for new functionality
|
||||
export interface TradeExecution {
|
||||
entry: number;
|
||||
exit: number;
|
||||
peak?: number;
|
||||
trough?: number;
|
||||
volume: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface MarketData {
|
||||
price: number;
|
||||
volume: number;
|
||||
timestamp: Date;
|
||||
bid?: number;
|
||||
ask?: number;
|
||||
bidSize?: number;
|
||||
askSize?: number;
|
||||
}
|
||||
|
||||
export interface BacktestResults {
|
||||
trades: TradeExecution[];
|
||||
equityCurve: Array<{ value: number; date: Date }>;
|
||||
|
||||
performance: PortfolioMetrics;
|
||||
riskMetrics: RiskMetrics;
|
||||
drawdownAnalysis: any; // Import from performance-metrics
|
||||
}
|
||||
|
||||
// Export all calculation functions
|
||||
export * from './basic-calculations';
|
||||
export * from './technical-indicators';
|
||||
export * from './risk-metrics';
|
||||
export * from './portfolio-analytics';
|
||||
export * from './options-pricing';
|
||||
export * from './position-sizing';
|
||||
export * from './performance-metrics';
|
||||
export * from './market-statistics';
|
||||
export * from './volatility-models';
|
||||
export * from './correlation-analysis';
|
||||
|
||||
// Import specific functions for convenience functions
|
||||
import {
|
||||
sma, ema, rsi, macd, bollingerBands, atr, stochastic,
|
||||
williamsR, cci, momentum, roc
|
||||
} from './technical-indicators';
|
||||
import { calculateRiskMetrics } from './risk-metrics';
|
||||
import { calculateStrategyMetrics } from './performance-metrics';
|
||||
|
||||
// Convenience function to calculate all technical indicators at once
|
||||
export function calculateAllTechnicalIndicators(
|
||||
ohlcv: OHLCVData[],
|
||||
periods: { sma?: number; ema?: number; rsi?: number; atr?: number } = {}
|
||||
): TechnicalIndicators {
|
||||
const {
|
||||
sma: smaPeriod = 20,
|
||||
ema: emaPeriod = 20,
|
||||
rsi: rsiPeriod = 14,
|
||||
atr: atrPeriod = 14
|
||||
} = periods;
|
||||
|
||||
const closes = ohlcv.map(d => d.close);
|
||||
|
||||
return {
|
||||
sma: sma(closes, smaPeriod),
|
||||
ema: ema(closes, emaPeriod),
|
||||
rsi: rsi(closes, rsiPeriod),
|
||||
macd: macd(closes),
|
||||
bollinger: bollingerBands(closes),
|
||||
atr: atr(ohlcv, atrPeriod),
|
||||
stochastic: stochastic(ohlcv),
|
||||
williams_r: williamsR(ohlcv),
|
||||
cci: cci(ohlcv),
|
||||
momentum: momentum(closes),
|
||||
roc: roc(closes)
|
||||
};
|
||||
}
|
||||
|
||||
// Convenience function for comprehensive portfolio analysis
|
||||
export function analyzePortfolio(
|
||||
returns: number[],
|
||||
equityCurve: Array<{ value: number; date: Date }>,
|
||||
benchmarkReturns?: number[],
|
||||
riskFreeRate: number = 0.02
|
||||
): {
|
||||
performance: PortfolioMetrics;
|
||||
risk: RiskMetrics;
|
||||
trades?: any;
|
||||
drawdown?: any;
|
||||
} {
|
||||
const performance = calculateStrategyMetrics(equityCurve, benchmarkReturns, riskFreeRate);
|
||||
const equityValues = equityCurve.map(point => point.value);
|
||||
const risk = calculateRiskMetrics(returns, equityValues, benchmarkReturns, riskFreeRate);
|
||||
|
||||
return {
|
||||
performance,
|
||||
risk
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Comprehensive Financial Calculations Library
|
||||
*
|
||||
* This module provides a complete set of financial calculations for trading and investment analysis.
|
||||
* Organized into logical categories for easy use and maintenance.
|
||||
*/
|
||||
|
||||
// Core interfaces for financial data
|
||||
export interface OHLCVData {
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface PriceData {
|
||||
price: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Financial calculation result interfaces
|
||||
export interface PortfolioMetrics {
|
||||
totalValue: number;
|
||||
totalReturn: number;
|
||||
totalReturnPercent: number;
|
||||
dailyReturn: number;
|
||||
dailyReturnPercent: number;
|
||||
maxDrawdown: number;
|
||||
sharpeRatio: number;
|
||||
beta: number;
|
||||
alpha: number;
|
||||
volatility: number;
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
var95: number; // Value at Risk 95%
|
||||
var99: number; // Value at Risk 99%
|
||||
cvar95: number; // Conditional VaR 95%
|
||||
maxDrawdown: number;
|
||||
volatility: number;
|
||||
downside_deviation: number;
|
||||
calmar_ratio: number;
|
||||
sortino_ratio: number;
|
||||
beta: number;
|
||||
alpha: number;
|
||||
sharpeRatio: number;
|
||||
treynorRatio: number;
|
||||
trackingError: number;
|
||||
informationRatio: number;
|
||||
}
|
||||
|
||||
export interface TechnicalIndicators {
|
||||
sma: number[];
|
||||
ema: number[];
|
||||
rsi: number[];
|
||||
macd: { macd: number[], signal: number[], histogram: number[] };
|
||||
bollinger: { upper: number[], middle: number[], lower: number[] };
|
||||
atr: number[];
|
||||
stochastic: { k: number[], d: number[] };
|
||||
williams_r: number[];
|
||||
cci: number[];
|
||||
momentum: number[];
|
||||
roc: number[];
|
||||
}
|
||||
|
||||
// Additional interfaces for new functionality
|
||||
export interface TradeExecution {
|
||||
entry: number;
|
||||
exit: number;
|
||||
peak?: number;
|
||||
trough?: number;
|
||||
volume: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface MarketData {
|
||||
price: number;
|
||||
volume: number;
|
||||
timestamp: Date;
|
||||
bid?: number;
|
||||
ask?: number;
|
||||
bidSize?: number;
|
||||
askSize?: number;
|
||||
}
|
||||
|
||||
export interface BacktestResults {
|
||||
trades: TradeExecution[];
|
||||
equityCurve: Array<{ value: number; date: Date }>;
|
||||
|
||||
performance: PortfolioMetrics;
|
||||
riskMetrics: RiskMetrics;
|
||||
drawdownAnalysis: any; // Import from performance-metrics
|
||||
}
|
||||
|
||||
// Export all calculation functions
|
||||
export * from './basic-calculations';
|
||||
export * from './technical-indicators';
|
||||
export * from './risk-metrics';
|
||||
export * from './portfolio-analytics';
|
||||
export * from './options-pricing';
|
||||
export * from './position-sizing';
|
||||
export * from './performance-metrics';
|
||||
export * from './market-statistics';
|
||||
export * from './volatility-models';
|
||||
export * from './correlation-analysis';
|
||||
|
||||
// Import specific functions for convenience functions
|
||||
import {
|
||||
sma, ema, rsi, macd, bollingerBands, atr, stochastic,
|
||||
williamsR, cci, momentum, roc
|
||||
} from './technical-indicators';
|
||||
import { calculateRiskMetrics } from './risk-metrics';
|
||||
import { calculateStrategyMetrics } from './performance-metrics';
|
||||
|
||||
// Convenience function to calculate all technical indicators at once
|
||||
export function calculateAllTechnicalIndicators(
|
||||
ohlcv: OHLCVData[],
|
||||
periods: { sma?: number; ema?: number; rsi?: number; atr?: number } = {}
|
||||
): TechnicalIndicators {
|
||||
const {
|
||||
sma: smaPeriod = 20,
|
||||
ema: emaPeriod = 20,
|
||||
rsi: rsiPeriod = 14,
|
||||
atr: atrPeriod = 14
|
||||
} = periods;
|
||||
|
||||
const closes = ohlcv.map(d => d.close);
|
||||
|
||||
return {
|
||||
sma: sma(closes, smaPeriod),
|
||||
ema: ema(closes, emaPeriod),
|
||||
rsi: rsi(closes, rsiPeriod),
|
||||
macd: macd(closes),
|
||||
bollinger: bollingerBands(closes),
|
||||
atr: atr(ohlcv, atrPeriod),
|
||||
stochastic: stochastic(ohlcv),
|
||||
williams_r: williamsR(ohlcv),
|
||||
cci: cci(ohlcv),
|
||||
momentum: momentum(closes),
|
||||
roc: roc(closes)
|
||||
};
|
||||
}
|
||||
|
||||
// Convenience function for comprehensive portfolio analysis
|
||||
export function analyzePortfolio(
|
||||
returns: number[],
|
||||
equityCurve: Array<{ value: number; date: Date }>,
|
||||
benchmarkReturns?: number[],
|
||||
riskFreeRate: number = 0.02
|
||||
): {
|
||||
performance: PortfolioMetrics;
|
||||
risk: RiskMetrics;
|
||||
trades?: any;
|
||||
drawdown?: any;
|
||||
} {
|
||||
const performance = calculateStrategyMetrics(equityCurve, benchmarkReturns, riskFreeRate);
|
||||
const equityValues = equityCurve.map(point => point.value);
|
||||
const risk = calculateRiskMetrics(returns, equityValues, benchmarkReturns, riskFreeRate);
|
||||
|
||||
return {
|
||||
performance,
|
||||
risk
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,375 +1,375 @@
|
|||
/**
|
||||
* Risk Metrics and Analysis
|
||||
* Comprehensive risk measurement tools for portfolio and trading analysis
|
||||
*/
|
||||
|
||||
import { RiskMetrics, treynorRatio } from './index';
|
||||
|
||||
/**
|
||||
* Calculate Value at Risk (VaR) using historical simulation
|
||||
*/
|
||||
export function valueAtRisk(returns: number[], confidenceLevel: number = 0.95): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const index = Math.floor((1 - confidenceLevel) * sortedReturns.length);
|
||||
|
||||
return sortedReturns[index] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Conditional Value at Risk (CVaR/Expected Shortfall)
|
||||
*/
|
||||
export function conditionalValueAtRisk(returns: number[], confidenceLevel: number = 0.95): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const cutoffIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length);
|
||||
|
||||
if (cutoffIndex === 0) return sortedReturns[0];
|
||||
|
||||
const tailReturns = sortedReturns.slice(0, cutoffIndex);
|
||||
return tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate parametric VaR using normal distribution
|
||||
*/
|
||||
export function parametricVaR(
|
||||
returns: number[],
|
||||
confidenceLevel: number = 0.95,
|
||||
portfolioValue: number = 1
|
||||
): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
// Z-score for confidence level (normal distribution)
|
||||
const zScore = getZScore(confidenceLevel);
|
||||
|
||||
return portfolioValue * (mean - zScore * stdDev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate maximum drawdown
|
||||
*/
|
||||
export function maxDrawdown(equityCurve: number[]): number {
|
||||
if (equityCurve.length < 2) return 0;
|
||||
|
||||
let maxDD = 0;
|
||||
let peak = equityCurve[0];
|
||||
|
||||
for (let i = 1; i < equityCurve.length; i++) {
|
||||
if (equityCurve[i] > peak) {
|
||||
peak = equityCurve[i];
|
||||
} else {
|
||||
const drawdown = (peak - equityCurve[i]) / peak;
|
||||
maxDD = Math.max(maxDD, drawdown);
|
||||
}
|
||||
}
|
||||
|
||||
return maxDD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate downside deviation
|
||||
*/
|
||||
export function downsideDeviation(returns: number[], targetReturn: number = 0): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const downsideReturns = returns.filter(ret => ret < targetReturn);
|
||||
|
||||
if (downsideReturns.length === 0) return 0;
|
||||
|
||||
const sumSquaredDownside = downsideReturns.reduce(
|
||||
(sum, ret) => sum + Math.pow(ret - targetReturn, 2),
|
||||
0
|
||||
);
|
||||
|
||||
return Math.sqrt(sumSquaredDownside / returns.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Sharpe ratio
|
||||
*/
|
||||
export function sharpeRatio(returns: number[], riskFreeRate: number = 0): number {
|
||||
if (returns.length < 2) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
return (mean - riskFreeRate) / stdDev;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate beta coefficient
|
||||
*/
|
||||
export function beta(portfolioReturns: number[], marketReturns: number[]): number {
|
||||
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const n = portfolioReturns.length;
|
||||
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / n;
|
||||
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / n;
|
||||
|
||||
let covariance = 0;
|
||||
let marketVariance = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const portfolioDiff = portfolioReturns[i] - portfolioMean;
|
||||
const marketDiff = marketReturns[i] - marketMean;
|
||||
|
||||
covariance += portfolioDiff * marketDiff;
|
||||
marketVariance += marketDiff * marketDiff;
|
||||
}
|
||||
|
||||
return marketVariance === 0 ? 0 : covariance / marketVariance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate alpha
|
||||
*/
|
||||
export function alpha(
|
||||
portfolioReturns: number[],
|
||||
marketReturns: number[],
|
||||
riskFreeRate: number = 0
|
||||
): number {
|
||||
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
|
||||
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length;
|
||||
const portfolioBeta = beta(portfolioReturns, marketReturns);
|
||||
|
||||
return portfolioMean - (riskFreeRate + portfolioBeta * (marketMean - riskFreeRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tracking error
|
||||
*/
|
||||
export function trackingError(portfolioReturns: number[], benchmarkReturns: number[]): number {
|
||||
if (portfolioReturns.length !== benchmarkReturns.length || portfolioReturns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const activeReturns = portfolioReturns.map((ret, i) => ret - benchmarkReturns[i]);
|
||||
const mean = activeReturns.reduce((sum, ret) => sum + ret, 0) / activeReturns.length;
|
||||
|
||||
const variance = activeReturns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (activeReturns.length - 1);
|
||||
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate volatility (standard deviation of returns)
|
||||
*/
|
||||
export function volatility(returns: number[]): number {
|
||||
if (returns.length < 2) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
|
||||
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate annualized volatility
|
||||
*/
|
||||
export function annualizedVolatility(returns: number[], periodsPerYear: number = 252): number {
|
||||
return volatility(returns) * Math.sqrt(periodsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate skewness (measure of asymmetry)
|
||||
*/
|
||||
export function skewness(returns: number[]): number {
|
||||
if (returns.length < 3) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
const skew = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 3), 0) / returns.length;
|
||||
|
||||
return skew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate kurtosis (measure of tail heaviness)
|
||||
*/
|
||||
export function kurtosis(returns: number[]): number {
|
||||
if (returns.length < 4) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
const kurt = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 4), 0) / returns.length;
|
||||
|
||||
return kurt - 3; // Excess kurtosis (subtract 3 for normal distribution baseline)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive risk metrics
|
||||
*/
|
||||
export function calculateRiskMetrics(
|
||||
returns: number[],
|
||||
equityCurve: number[],
|
||||
marketReturns?: number[],
|
||||
riskFreeRate: number = 0
|
||||
): RiskMetrics {
|
||||
if (returns.length === 0) {
|
||||
return {
|
||||
var95: 0,
|
||||
var99: 0,
|
||||
cvar95: 0,
|
||||
maxDrawdown: 0,
|
||||
volatility: 0,
|
||||
downside_deviation: 0,
|
||||
calmar_ratio: 0,
|
||||
sortino_ratio: 0,
|
||||
beta: 0,
|
||||
alpha: 0,
|
||||
sharpeRatio: 0,
|
||||
treynorRatio: 0,
|
||||
trackingError: 0,
|
||||
informationRatio: 0
|
||||
};
|
||||
}
|
||||
|
||||
const portfolioVolatility = volatility(returns);
|
||||
const portfolioMean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
// Calculate VaR
|
||||
const var95Value = valueAtRisk(returns, 0.95);
|
||||
const var99Value = valueAtRisk(returns, 0.99);
|
||||
const cvar95Value = conditionalValueAtRisk(returns, 0.95);
|
||||
|
||||
// Calculate max drawdown
|
||||
const maxDD = maxDrawdown(equityCurve);
|
||||
|
||||
// Calculate downside deviation
|
||||
const downsideDeviationValue = downsideDeviation(returns);
|
||||
// Calculate ratios
|
||||
const calmarRatio = maxDD > 0 ? portfolioMean / maxDD : 0;
|
||||
const sortinoRatio = downsideDeviationValue > 0 ? (portfolioMean - riskFreeRate) / downsideDeviationValue : 0;
|
||||
const sharpeRatio = portfolioVolatility > 0 ? (portfolioMean - riskFreeRate) / portfolioVolatility : 0;
|
||||
|
||||
let portfolioBeta = 0;
|
||||
let portfolioAlpha = 0;
|
||||
let portfolioTreynorRatio = 0;
|
||||
let portfolioTrackingError = 0;
|
||||
let informationRatio = 0;
|
||||
|
||||
if (marketReturns && marketReturns.length === returns.length) {
|
||||
portfolioBeta = beta(returns, marketReturns);
|
||||
portfolioAlpha = alpha(returns, marketReturns, riskFreeRate);
|
||||
portfolioTreynorRatio = treynorRatio(returns, marketReturns, riskFreeRate);
|
||||
portfolioTrackingError = trackingError(returns, marketReturns);
|
||||
informationRatio = portfolioTrackingError > 0 ? portfolioAlpha / portfolioTrackingError : 0;
|
||||
}
|
||||
return {
|
||||
var95: var95Value,
|
||||
var99: var99Value,
|
||||
cvar95: cvar95Value,
|
||||
maxDrawdown: maxDD,
|
||||
volatility: portfolioVolatility,
|
||||
downside_deviation: downsideDeviationValue,
|
||||
calmar_ratio: calmarRatio,
|
||||
sortino_ratio: sortinoRatio,
|
||||
beta: portfolioBeta,
|
||||
alpha: portfolioAlpha,
|
||||
sharpeRatio,
|
||||
treynorRatio: portfolioTreynorRatio,
|
||||
trackingError: portfolioTrackingError,
|
||||
informationRatio
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get Z-score for confidence level
|
||||
* This implementation handles arbitrary confidence levels
|
||||
*/
|
||||
function getZScore(confidenceLevel: number): number {
|
||||
// First check our lookup table for common values (more precise)
|
||||
const zScores: { [key: string]: number } = {
|
||||
'0.90': 1.282,
|
||||
'0.95': 1.645,
|
||||
'0.975': 1.960,
|
||||
'0.99': 2.326,
|
||||
'0.995': 2.576
|
||||
};
|
||||
|
||||
const key = confidenceLevel.toString();
|
||||
if (zScores[key]) return zScores[key];
|
||||
|
||||
// For arbitrary confidence levels, use approximation
|
||||
if (confidenceLevel < 0.5) return -getZScore(1 - confidenceLevel);
|
||||
|
||||
if (confidenceLevel >= 0.999) return 3.09; // Cap at 99.9% for numerical stability
|
||||
|
||||
// Approximation of inverse normal CDF
|
||||
const y = Math.sqrt(-2.0 * Math.log(1.0 - confidenceLevel));
|
||||
return y - (2.515517 + 0.802853 * y + 0.010328 * y * y) /
|
||||
(1.0 + 1.432788 * y + 0.189269 * y * y + 0.001308 * y * y * y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate portfolio risk contribution
|
||||
*/
|
||||
export function riskContribution(
|
||||
weights: number[],
|
||||
covarianceMatrix: number[][],
|
||||
portfolioVolatility: number
|
||||
): number[] {
|
||||
const n = weights.length;
|
||||
const contributions: number[] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
let marginalContribution = 0;
|
||||
|
||||
for (let j = 0; j < n; j++) {
|
||||
marginalContribution += weights[j] * covarianceMatrix[i][j];
|
||||
}
|
||||
|
||||
const contribution = (weights[i] * marginalContribution) / Math.pow(portfolioVolatility, 2);
|
||||
contributions.push(contribution);
|
||||
}
|
||||
|
||||
return contributions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Ulcer Index
|
||||
*/
|
||||
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
|
||||
let sumSquaredDrawdown = 0;
|
||||
let peak = equityCurve[0].value;
|
||||
|
||||
for (const point of equityCurve) {
|
||||
peak = Math.max(peak, point.value);
|
||||
const drawdownPercent = (peak - point.value) / peak * 100;
|
||||
sumSquaredDrawdown += drawdownPercent * drawdownPercent;
|
||||
}
|
||||
|
||||
return Math.sqrt(sumSquaredDrawdown / equityCurve.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk-adjusted return (RAR)
|
||||
*/
|
||||
export function riskAdjustedReturn(
|
||||
portfolioReturn: number,
|
||||
portfolioRisk: number,
|
||||
riskFreeRate: number = 0
|
||||
): number {
|
||||
if (portfolioRisk === 0) return 0;
|
||||
return (portfolioReturn - riskFreeRate) / portfolioRisk;
|
||||
}
|
||||
/**
|
||||
* Risk Metrics and Analysis
|
||||
* Comprehensive risk measurement tools for portfolio and trading analysis
|
||||
*/
|
||||
|
||||
import { RiskMetrics, treynorRatio } from './index';
|
||||
|
||||
/**
|
||||
* Calculate Value at Risk (VaR) using historical simulation
|
||||
*/
|
||||
export function valueAtRisk(returns: number[], confidenceLevel: number = 0.95): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const index = Math.floor((1 - confidenceLevel) * sortedReturns.length);
|
||||
|
||||
return sortedReturns[index] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Conditional Value at Risk (CVaR/Expected Shortfall)
|
||||
*/
|
||||
export function conditionalValueAtRisk(returns: number[], confidenceLevel: number = 0.95): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const sortedReturns = [...returns].sort((a, b) => a - b);
|
||||
const cutoffIndex = Math.floor((1 - confidenceLevel) * sortedReturns.length);
|
||||
|
||||
if (cutoffIndex === 0) return sortedReturns[0];
|
||||
|
||||
const tailReturns = sortedReturns.slice(0, cutoffIndex);
|
||||
return tailReturns.reduce((sum, ret) => sum + ret, 0) / tailReturns.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate parametric VaR using normal distribution
|
||||
*/
|
||||
export function parametricVaR(
|
||||
returns: number[],
|
||||
confidenceLevel: number = 0.95,
|
||||
portfolioValue: number = 1
|
||||
): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
// Z-score for confidence level (normal distribution)
|
||||
const zScore = getZScore(confidenceLevel);
|
||||
|
||||
return portfolioValue * (mean - zScore * stdDev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate maximum drawdown
|
||||
*/
|
||||
export function maxDrawdown(equityCurve: number[]): number {
|
||||
if (equityCurve.length < 2) return 0;
|
||||
|
||||
let maxDD = 0;
|
||||
let peak = equityCurve[0];
|
||||
|
||||
for (let i = 1; i < equityCurve.length; i++) {
|
||||
if (equityCurve[i] > peak) {
|
||||
peak = equityCurve[i];
|
||||
} else {
|
||||
const drawdown = (peak - equityCurve[i]) / peak;
|
||||
maxDD = Math.max(maxDD, drawdown);
|
||||
}
|
||||
}
|
||||
|
||||
return maxDD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate downside deviation
|
||||
*/
|
||||
export function downsideDeviation(returns: number[], targetReturn: number = 0): number {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const downsideReturns = returns.filter(ret => ret < targetReturn);
|
||||
|
||||
if (downsideReturns.length === 0) return 0;
|
||||
|
||||
const sumSquaredDownside = downsideReturns.reduce(
|
||||
(sum, ret) => sum + Math.pow(ret - targetReturn, 2),
|
||||
0
|
||||
);
|
||||
|
||||
return Math.sqrt(sumSquaredDownside / returns.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Sharpe ratio
|
||||
*/
|
||||
export function sharpeRatio(returns: number[], riskFreeRate: number = 0): number {
|
||||
if (returns.length < 2) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
return (mean - riskFreeRate) / stdDev;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate beta coefficient
|
||||
*/
|
||||
export function beta(portfolioReturns: number[], marketReturns: number[]): number {
|
||||
if (portfolioReturns.length !== marketReturns.length || portfolioReturns.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const n = portfolioReturns.length;
|
||||
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / n;
|
||||
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / n;
|
||||
|
||||
let covariance = 0;
|
||||
let marketVariance = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const portfolioDiff = portfolioReturns[i] - portfolioMean;
|
||||
const marketDiff = marketReturns[i] - marketMean;
|
||||
|
||||
covariance += portfolioDiff * marketDiff;
|
||||
marketVariance += marketDiff * marketDiff;
|
||||
}
|
||||
|
||||
return marketVariance === 0 ? 0 : covariance / marketVariance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate alpha
|
||||
*/
|
||||
export function alpha(
|
||||
portfolioReturns: number[],
|
||||
marketReturns: number[],
|
||||
riskFreeRate: number = 0
|
||||
): number {
|
||||
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
|
||||
const marketMean = marketReturns.reduce((sum, ret) => sum + ret, 0) / marketReturns.length;
|
||||
const portfolioBeta = beta(portfolioReturns, marketReturns);
|
||||
|
||||
return portfolioMean - (riskFreeRate + portfolioBeta * (marketMean - riskFreeRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tracking error
|
||||
*/
|
||||
export function trackingError(portfolioReturns: number[], benchmarkReturns: number[]): number {
|
||||
if (portfolioReturns.length !== benchmarkReturns.length || portfolioReturns.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const activeReturns = portfolioReturns.map((ret, i) => ret - benchmarkReturns[i]);
|
||||
const mean = activeReturns.reduce((sum, ret) => sum + ret, 0) / activeReturns.length;
|
||||
|
||||
const variance = activeReturns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (activeReturns.length - 1);
|
||||
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate volatility (standard deviation of returns)
|
||||
*/
|
||||
export function volatility(returns: number[]): number {
|
||||
if (returns.length < 2) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / (returns.length - 1);
|
||||
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate annualized volatility
|
||||
*/
|
||||
export function annualizedVolatility(returns: number[], periodsPerYear: number = 252): number {
|
||||
return volatility(returns) * Math.sqrt(periodsPerYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate skewness (measure of asymmetry)
|
||||
*/
|
||||
export function skewness(returns: number[]): number {
|
||||
if (returns.length < 3) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
const skew = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 3), 0) / returns.length;
|
||||
|
||||
return skew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate kurtosis (measure of tail heaviness)
|
||||
*/
|
||||
export function kurtosis(returns: number[]): number {
|
||||
if (returns.length < 4) return 0;
|
||||
|
||||
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
const kurt = returns.reduce((sum, ret) => sum + Math.pow((ret - mean) / stdDev, 4), 0) / returns.length;
|
||||
|
||||
return kurt - 3; // Excess kurtosis (subtract 3 for normal distribution baseline)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive risk metrics
|
||||
*/
|
||||
export function calculateRiskMetrics(
|
||||
returns: number[],
|
||||
equityCurve: number[],
|
||||
marketReturns?: number[],
|
||||
riskFreeRate: number = 0
|
||||
): RiskMetrics {
|
||||
if (returns.length === 0) {
|
||||
return {
|
||||
var95: 0,
|
||||
var99: 0,
|
||||
cvar95: 0,
|
||||
maxDrawdown: 0,
|
||||
volatility: 0,
|
||||
downside_deviation: 0,
|
||||
calmar_ratio: 0,
|
||||
sortino_ratio: 0,
|
||||
beta: 0,
|
||||
alpha: 0,
|
||||
sharpeRatio: 0,
|
||||
treynorRatio: 0,
|
||||
trackingError: 0,
|
||||
informationRatio: 0
|
||||
};
|
||||
}
|
||||
|
||||
const portfolioVolatility = volatility(returns);
|
||||
const portfolioMean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
||||
// Calculate VaR
|
||||
const var95Value = valueAtRisk(returns, 0.95);
|
||||
const var99Value = valueAtRisk(returns, 0.99);
|
||||
const cvar95Value = conditionalValueAtRisk(returns, 0.95);
|
||||
|
||||
// Calculate max drawdown
|
||||
const maxDD = maxDrawdown(equityCurve);
|
||||
|
||||
// Calculate downside deviation
|
||||
const downsideDeviationValue = downsideDeviation(returns);
|
||||
// Calculate ratios
|
||||
const calmarRatio = maxDD > 0 ? portfolioMean / maxDD : 0;
|
||||
const sortinoRatio = downsideDeviationValue > 0 ? (portfolioMean - riskFreeRate) / downsideDeviationValue : 0;
|
||||
const sharpeRatio = portfolioVolatility > 0 ? (portfolioMean - riskFreeRate) / portfolioVolatility : 0;
|
||||
|
||||
let portfolioBeta = 0;
|
||||
let portfolioAlpha = 0;
|
||||
let portfolioTreynorRatio = 0;
|
||||
let portfolioTrackingError = 0;
|
||||
let informationRatio = 0;
|
||||
|
||||
if (marketReturns && marketReturns.length === returns.length) {
|
||||
portfolioBeta = beta(returns, marketReturns);
|
||||
portfolioAlpha = alpha(returns, marketReturns, riskFreeRate);
|
||||
portfolioTreynorRatio = treynorRatio(returns, marketReturns, riskFreeRate);
|
||||
portfolioTrackingError = trackingError(returns, marketReturns);
|
||||
informationRatio = portfolioTrackingError > 0 ? portfolioAlpha / portfolioTrackingError : 0;
|
||||
}
|
||||
return {
|
||||
var95: var95Value,
|
||||
var99: var99Value,
|
||||
cvar95: cvar95Value,
|
||||
maxDrawdown: maxDD,
|
||||
volatility: portfolioVolatility,
|
||||
downside_deviation: downsideDeviationValue,
|
||||
calmar_ratio: calmarRatio,
|
||||
sortino_ratio: sortinoRatio,
|
||||
beta: portfolioBeta,
|
||||
alpha: portfolioAlpha,
|
||||
sharpeRatio,
|
||||
treynorRatio: portfolioTreynorRatio,
|
||||
trackingError: portfolioTrackingError,
|
||||
informationRatio
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get Z-score for confidence level
|
||||
* This implementation handles arbitrary confidence levels
|
||||
*/
|
||||
function getZScore(confidenceLevel: number): number {
|
||||
// First check our lookup table for common values (more precise)
|
||||
const zScores: { [key: string]: number } = {
|
||||
'0.90': 1.282,
|
||||
'0.95': 1.645,
|
||||
'0.975': 1.960,
|
||||
'0.99': 2.326,
|
||||
'0.995': 2.576
|
||||
};
|
||||
|
||||
const key = confidenceLevel.toString();
|
||||
if (zScores[key]) return zScores[key];
|
||||
|
||||
// For arbitrary confidence levels, use approximation
|
||||
if (confidenceLevel < 0.5) return -getZScore(1 - confidenceLevel);
|
||||
|
||||
if (confidenceLevel >= 0.999) return 3.09; // Cap at 99.9% for numerical stability
|
||||
|
||||
// Approximation of inverse normal CDF
|
||||
const y = Math.sqrt(-2.0 * Math.log(1.0 - confidenceLevel));
|
||||
return y - (2.515517 + 0.802853 * y + 0.010328 * y * y) /
|
||||
(1.0 + 1.432788 * y + 0.189269 * y * y + 0.001308 * y * y * y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate portfolio risk contribution
|
||||
*/
|
||||
export function riskContribution(
|
||||
weights: number[],
|
||||
covarianceMatrix: number[][],
|
||||
portfolioVolatility: number
|
||||
): number[] {
|
||||
const n = weights.length;
|
||||
const contributions: number[] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
let marginalContribution = 0;
|
||||
|
||||
for (let j = 0; j < n; j++) {
|
||||
marginalContribution += weights[j] * covarianceMatrix[i][j];
|
||||
}
|
||||
|
||||
const contribution = (weights[i] * marginalContribution) / Math.pow(portfolioVolatility, 2);
|
||||
contributions.push(contribution);
|
||||
}
|
||||
|
||||
return contributions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Ulcer Index
|
||||
*/
|
||||
export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): number {
|
||||
let sumSquaredDrawdown = 0;
|
||||
let peak = equityCurve[0].value;
|
||||
|
||||
for (const point of equityCurve) {
|
||||
peak = Math.max(peak, point.value);
|
||||
const drawdownPercent = (peak - point.value) / peak * 100;
|
||||
sumSquaredDrawdown += drawdownPercent * drawdownPercent;
|
||||
}
|
||||
|
||||
return Math.sqrt(sumSquaredDrawdown / equityCurve.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk-adjusted return (RAR)
|
||||
*/
|
||||
export function riskAdjustedReturn(
|
||||
portfolioReturn: number,
|
||||
portfolioRisk: number,
|
||||
riskFreeRate: number = 0
|
||||
): number {
|
||||
if (portfolioRisk === 0) return 0;
|
||||
return (portfolioReturn - riskFreeRate) / portfolioRisk;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,55 +1,55 @@
|
|||
/**
|
||||
* Date and time utilities for working with market data
|
||||
*/
|
||||
export const dateUtils = {
|
||||
/**
|
||||
* Check if a date is a trading day (Monday-Friday, non-holiday)
|
||||
* This is a simplified implementation - a real version would check market holidays
|
||||
*/
|
||||
isTradingDay(date: Date): boolean {
|
||||
const day = date.getDay();
|
||||
return day > 0 && day < 6; // Mon-Fri
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the next trading day from a given date
|
||||
*/
|
||||
getNextTradingDay(date: Date): Date {
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
while (!this.isTradingDay(nextDay)) {
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
}
|
||||
|
||||
return nextDay;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the previous trading day from a given date
|
||||
*/
|
||||
getPreviousTradingDay(date: Date): Date {
|
||||
const prevDay = new Date(date);
|
||||
prevDay.setDate(prevDay.getDate() - 1);
|
||||
|
||||
while (!this.isTradingDay(prevDay)) {
|
||||
prevDay.setDate(prevDay.getDate() - 1);
|
||||
}
|
||||
|
||||
return prevDay;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format a date as YYYY-MM-DD
|
||||
*/
|
||||
formatDate(date: Date): string {
|
||||
return date.toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a date string in YYYY-MM-DD format
|
||||
*/
|
||||
parseDate(dateStr: string): Date {
|
||||
return new Date(dateStr);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Date and time utilities for working with market data
|
||||
*/
|
||||
export const dateUtils = {
|
||||
/**
|
||||
* Check if a date is a trading day (Monday-Friday, non-holiday)
|
||||
* This is a simplified implementation - a real version would check market holidays
|
||||
*/
|
||||
isTradingDay(date: Date): boolean {
|
||||
const day = date.getDay();
|
||||
return day > 0 && day < 6; // Mon-Fri
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the next trading day from a given date
|
||||
*/
|
||||
getNextTradingDay(date: Date): Date {
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
while (!this.isTradingDay(nextDay)) {
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
}
|
||||
|
||||
return nextDay;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the previous trading day from a given date
|
||||
*/
|
||||
getPreviousTradingDay(date: Date): Date {
|
||||
const prevDay = new Date(date);
|
||||
prevDay.setDate(prevDay.getDate() - 1);
|
||||
|
||||
while (!this.isTradingDay(prevDay)) {
|
||||
prevDay.setDate(prevDay.getDate() - 1);
|
||||
}
|
||||
|
||||
return prevDay;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format a date as YYYY-MM-DD
|
||||
*/
|
||||
formatDate(date: Date): string {
|
||||
return date.toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a date string in YYYY-MM-DD format
|
||||
*/
|
||||
parseDate(dateStr: string): Date {
|
||||
return new Date(dateStr);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './dateUtils';
|
||||
export * from './dateUtils';
|
||||
export * from './calculations/index';
|
||||
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../types" },
|
||||
{ "path": "../config" },
|
||||
{ "path": "../logger" }
|
||||
]
|
||||
}
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{ "path": "../types" },
|
||||
{ "path": "../config" },
|
||||
{ "path": "../logger" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["@stock-bot/types#build", "@stock-bot/config#build"],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["@stock-bot/types#build", "@stock-bot/config#build"],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue