UNPKG

herta

Version:

Advanced mathematics framework for scientific, engineering, and financial applications

484 lines (422 loc) 14.2 kB
/** * Trading Strategies module for herta.js * Provides implementations of quantitative trading strategies * for forex and cryptocurrency trading */ const statistics = require('../core/statistics'); const technicalAnalysis = require('./technicalAnalysis'); const tradingStrategies = {}; /** * Calculate optimal position size based on Kelly Criterion * @param {number} winRate - Probability of winning (0-1) * @param {number} winLossRatio - Ratio of average win to average loss * @param {number} capitalAmount - Total capital available * @param {number} maxRiskPercent - Maximum risk percentage (optional) * @returns {number} - Optimal position size */ tradingStrategies.kellyPositionSize = function (winRate, winLossRatio, capitalAmount, maxRiskPercent = 0.2) { // Kelly fraction = (p * b - q) / b // where p = probability of win, q = probability of loss (1-p), b = win/loss ratio const kellyFraction = (winRate * winLossRatio - (1 - winRate)) / winLossRatio; // Limit the maximum risk using the half-Kelly or specified max percentage const adjustedFraction = Math.min(kellyFraction, maxRiskPercent); // Return the position size (can't be negative) return Math.max(0, adjustedFraction * capitalAmount); }; /** * Calculate optimal stop loss and take profit levels based on volatility * @param {number} entryPrice - Entry price of the position * @param {Array} prices - Historical price data * @param {number} atrMultiplier - Multiplier for ATR (Average True Range) * @param {number} riskRewardRatio - Desired risk-to-reward ratio * @returns {Object} - Stop loss and take profit prices */ tradingStrategies.volatilityBasedLevels = function (entryPrice, prices, atrMultiplier = 2, riskRewardRatio = 2) { if (prices.length < 15) { throw new Error('Insufficient historical data'); } // Extract OHLC data const highPrices = prices.map((p) => p.high || p); const lowPrices = prices.map((p) => p.low || p); const closePrices = prices.map((p) => p.close || p); // Calculate ATR const atr = technicalAnalysis.atr(highPrices, lowPrices, closePrices, 14).slice(-1)[0]; // Calculate stop loss distance based on ATR const stopDistance = atr * atrMultiplier; // Calculate stop loss and take profit levels const isLong = true; // Assume long position for simplicity const stopLoss = isLong ? entryPrice - stopDistance : entryPrice + stopDistance; const takeProfit = isLong ? entryPrice + (stopDistance * riskRewardRatio) : entryPrice - (stopDistance * riskRewardRatio); return { entryPrice, stopLoss, takeProfit, riskAmount: Math.abs(entryPrice - stopLoss), rewardAmount: Math.abs(takeProfit - entryPrice) }; }; /** * Evaluate performance of a trading strategy based on historical data * @param {Array} trades - Array of trade objects with entry, exit, and type (long/short) properties * @param {number} initialCapital - Initial capital amount * @returns {Object} - Performance metrics */ tradingStrategies.evaluatePerformance = function (trades, initialCapital = 10000) { if (!trades.length) { throw new Error('No trades provided for evaluation'); } let capital = initialCapital; let highWaterMark = initialCapital; let maxDrawdown = 0; let winCount = 0; let lossCount = 0; const returns = []; // Process each trade for (const trade of trades) { const { entryPrice } = trade; const { exitPrice } = trade; const size = trade.size || 1; // Position size (units/contracts) const isLong = trade.type === 'long'; // Calculate profit/loss const pnl = isLong ? (exitPrice - entryPrice) * size : (entryPrice - exitPrice) * size; // Update capital capital += pnl; // Update high water mark and max drawdown if (capital > highWaterMark) { highWaterMark = capital; } else { const drawdown = (highWaterMark - capital) / highWaterMark; if (drawdown > maxDrawdown) { maxDrawdown = drawdown; } } // Update win/loss count if (pnl > 0) { winCount++; } else if (pnl < 0) { lossCount++; } // Calculate return for this trade returns.push(pnl / initialCapital); } // Calculate performance metrics const totalReturns = (capital - initialCapital) / initialCapital; const winRate = winCount / trades.length; // Calculate Sharpe Ratio (assuming annual and risk-free rate of 0) const meanReturn = returns.reduce((a, b) => a + b, 0) / returns.length; const stdDevReturn = Math.sqrt( returns.reduce((sum, value) => sum + (value - meanReturn) ** 2, 0) / returns.length ); const sharpeRatio = meanReturn / stdDevReturn * Math.sqrt(252); // Annualized return { finalCapital: capital, totalReturns, totalPnL: capital - initialCapital, maxDrawdown, winRate, winCount, lossCount, tradeCount: trades.length, sharpeRatio }; }; /** * Trend Following strategy implementation * @param {Array} prices - Historical price array * @param {Object} params - Strategy parameters * @returns {Array} - Array of trade signals */ tradingStrategies.trendFollowing = function (prices, params = {}) { const { shortPeriod = 20, longPeriod = 50, stopLossPercent = 0.02 } = params; if (prices.length < longPeriod + 10) { throw new Error('Insufficient historical data'); } const closePrices = prices.map((p) => p.close || p); // Calculate moving averages const shortMA = technicalAnalysis.sma(closePrices, shortPeriod); const longMA = technicalAnalysis.sma(closePrices, longPeriod); // Align data (longMA will be shorter) const diff = shortPeriod - longPeriod; const shortMAAligned = shortMA.slice(Math.abs(diff)); const longMAAligned = longMA; // Generate signals const signals = []; let position = null; for (let i = 1; i < longMAAligned.length; i++) { const crossingUp = shortMAAligned[i - 1] <= longMAAligned[i - 1] && shortMAAligned[i] > longMAAligned[i]; const crossingDown = shortMAAligned[i - 1] >= longMAAligned[i - 1] && shortMAAligned[i] < longMAAligned[i]; // Calculate actual price index const priceIndex = i + longPeriod - 1; const currentPrice = closePrices[priceIndex]; // Check stop loss if in position if (position) { const stopLossHit = position.type === 'long' ? currentPrice < position.entryPrice * (1 - stopLossPercent) : currentPrice > position.entryPrice * (1 + stopLossPercent); if (stopLossHit) { signals.push({ type: 'exit', reason: 'stop_loss', price: currentPrice, time: priceIndex, position: position.type }); position = null; continue; } } // Entry signals if (!position && crossingUp) { position = { type: 'long', entryPrice: currentPrice, entryTime: priceIndex }; signals.push({ type: 'entry', direction: 'long', price: currentPrice, time: priceIndex }); } else if (!position && crossingDown) { position = { type: 'short', entryPrice: currentPrice, entryTime: priceIndex }; signals.push({ type: 'entry', direction: 'short', price: currentPrice, time: priceIndex }); } // Exit signals else if (position && position.type === 'long' && crossingDown) { signals.push({ type: 'exit', reason: 'signal', price: currentPrice, time: priceIndex, position: 'long' }); position = null; } else if (position && position.type === 'short' && crossingUp) { signals.push({ type: 'exit', reason: 'signal', price: currentPrice, time: priceIndex, position: 'short' }); position = null; } } return signals; }; /** * Mean Reversion strategy implementation * @param {Array} prices - Historical price array * @param {Object} params - Strategy parameters * @returns {Array} - Array of trade signals */ tradingStrategies.meanReversion = function (prices, params = {}) { const { lookbackPeriod = 20, entryThreshold = 2.0, exitThreshold = 0.5, maxHoldingDays = 10 } = params; if (prices.length < lookbackPeriod + 10) { throw new Error('Insufficient historical data'); } const closePrices = prices.map((p) => p.close || p); // Calculate Bollinger Bands const { upperBand, middleBand, lowerBand } = technicalAnalysis.bollingerBands(closePrices, lookbackPeriod, entryThreshold); // Generate signals const signals = []; let position = null; for (let i = 0; i < upperBand.length; i++) { // Calculate actual price index const priceIndex = i + lookbackPeriod - 1; const currentPrice = closePrices[priceIndex]; // Check if we've held a position too long if (position && (priceIndex - position.entryTime) >= maxHoldingDays) { signals.push({ type: 'exit', reason: 'time_limit', price: currentPrice, time: priceIndex, position: position.type }); position = null; } // Entry signals if (!position && currentPrice > upperBand[i]) { // Price above upper band - enter short position = { type: 'short', entryPrice: currentPrice, entryTime: priceIndex, middleBandAtEntry: middleBand[i] }; signals.push({ type: 'entry', direction: 'short', price: currentPrice, time: priceIndex }); } else if (!position && currentPrice < lowerBand[i]) { // Price below lower band - enter long position = { type: 'long', entryPrice: currentPrice, entryTime: priceIndex, middleBandAtEntry: middleBand[i] }; signals.push({ type: 'entry', direction: 'long', price: currentPrice, time: priceIndex }); } // Exit signals else if (position && position.type === 'short' && currentPrice <= position.middleBandAtEntry) { signals.push({ type: 'exit', reason: 'target', price: currentPrice, time: priceIndex, position: 'short' }); position = null; } else if (position && position.type === 'long' && currentPrice >= position.middleBandAtEntry) { signals.push({ type: 'exit', reason: 'target', price: currentPrice, time: priceIndex, position: 'long' }); position = null; } } return signals; }; /** * Pair Trading strategy implementation * @param {Array} pricesA - Historical price array for first asset * @param {Array} pricesB - Historical price array for second asset * @param {Object} params - Strategy parameters * @returns {Array} - Array of trade signals */ tradingStrategies.pairTrading = function (pricesA, pricesB, params = {}) { const { lookbackPeriod = 30, entryThreshold = 2.0, exitThreshold = 0.5 } = params; if (pricesA.length !== pricesB.length || pricesA.length < lookbackPeriod + 10) { throw new Error('Invalid price data for pair trading'); } const closePricesA = pricesA.map((p) => p.close || p); const closePricesB = pricesB.map((p) => p.close || p); // Calculate price ratio const priceRatio = closePricesA.map((price, i) => price / closePricesB[i]); // Calculate z-score of the ratio const zScores = []; for (let i = lookbackPeriod - 1; i < priceRatio.length; i++) { const ratioWindow = priceRatio.slice(i - lookbackPeriod + 1, i + 1); const mean = ratioWindow.reduce((sum, val) => sum + val, 0) / lookbackPeriod; const stdDev = Math.sqrt( ratioWindow.reduce((sum, val) => sum + (val - mean) ** 2, 0) / lookbackPeriod ); zScores.push((priceRatio[i] - mean) / (stdDev || 1)); // Avoid division by zero } // Generate signals const signals = []; let position = null; for (let i = 0; i < zScores.length; i++) { const priceIndex = i + lookbackPeriod - 1; const currentZScore = zScores[i]; // Entry signals if (!position && currentZScore > entryThreshold) { // Ratio is too high - short A, long B position = { type: 'divergence', direction: 'ratio_short', entryZScore: currentZScore, entryTime: priceIndex }; signals.push({ type: 'entry', direction: 'ratio_short', priceA: closePricesA[priceIndex], priceB: closePricesB[priceIndex], ratio: priceRatio[priceIndex], zScore: currentZScore, time: priceIndex }); } else if (!position && currentZScore < -entryThreshold) { // Ratio is too low - long A, short B position = { type: 'divergence', direction: 'ratio_long', entryZScore: currentZScore, entryTime: priceIndex }; signals.push({ type: 'entry', direction: 'ratio_long', priceA: closePricesA[priceIndex], priceB: closePricesB[priceIndex], ratio: priceRatio[priceIndex], zScore: currentZScore, time: priceIndex }); } // Exit signals else if (position && position.direction === 'ratio_short' && currentZScore <= exitThreshold) { signals.push({ type: 'exit', reason: 'convergence', priceA: closePricesA[priceIndex], priceB: closePricesB[priceIndex], ratio: priceRatio[priceIndex], zScore: currentZScore, time: priceIndex, position: 'ratio_short' }); position = null; } else if (position && position.direction === 'ratio_long' && currentZScore >= -exitThreshold) { signals.push({ type: 'exit', reason: 'convergence', priceA: closePricesA[priceIndex], priceB: closePricesB[priceIndex], ratio: priceRatio[priceIndex], zScore: currentZScore, time: priceIndex, position: 'ratio_long' }); position = null; } } return signals; }; module.exports = tradingStrategies;