diff --git a/tests/unit/memory/adaptive-thresholds.test.ts b/tests/unit/memory/adaptive-thresholds.test.ts new file mode 100644 index 0000000..c045026 --- /dev/null +++ b/tests/unit/memory/adaptive-thresholds.test.ts @@ -0,0 +1,517 @@ +/** + * Adaptive Thresholds Unit Tests + */ + +import { AdaptiveThresholds, type AdaptiveThresholdConfig } from '../../../src/services/memory/adaptive-thresholds'; +import type { ScalperConfig } from '../../../src/types'; + +describe('AdaptiveThresholds', () => { + let thresholds: AdaptiveThresholds; + let baseConfig: ScalperConfig; + + beforeEach(() => { + thresholds = new AdaptiveThresholds(); + baseConfig = { + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + } as ScalperConfig; + }); + + describe('Constructor', () => { + it('should initialize with default config', () => { + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(0); + expect(stats.smoothedWinRate).toBe(0.5); + }); + + it('should accept custom config', () => { + const customThresholds = new AdaptiveThresholds({ + enabled: false, + minTradesForAdjustment: 20, + recentTradesWindow: 30, + smoothingFactor: 0.5, + }); + + const stats = customThresholds.getStats(); + expect(stats).toBeDefined(); + }); + + it('should use default values for partial config', () => { + const partialThresholds = new AdaptiveThresholds({ + minTradesForAdjustment: 15, + }); + + const stats = partialThresholds.getStats(); + expect(stats).toBeDefined(); + }); + }); + + describe('Disabled State', () => { + beforeEach(() => { + thresholds = new AdaptiveThresholds({ enabled: false }); + }); + + it('should not record trades when disabled', () => { + thresholds.recordTradeOutcome(true); + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(0); + }); + + it('should return base config when disabled', () => { + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result.minCombinedConfidence).toBe(baseConfig.minCombinedConfidence); + expect(result.adjustmentReason).toBe('Adaptive thresholds disabled'); + }); + }); + + describe('Recording Trade Outcomes', () => { + it('should record a winning trade', () => { + thresholds.recordTradeOutcome(true); + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(1); + expect(stats.recentWins).toBe(1); + expect(stats.winRate).toBe(1.0); + }); + + it('should record a losing trade', () => { + thresholds.recordTradeOutcome(false); + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(1); + expect(stats.recentWins).toBe(0); + expect(stats.winRate).toBe(0); + }); + + it('should record multiple trades', () => { + // 7 wins, 3 losses + for (let i = 0; i < 7; i++) { + thresholds.recordTradeOutcome(true); + } + for (let i = 0; i < 3; i++) { + thresholds.recordTradeOutcome(false); + } + + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(10); + expect(stats.recentWins).toBe(7); + expect(stats.winRate).toBe(0.7); + }); + + it('should calculate smoothed win rate using EMA', () => { + // Record trades to test EMA smoothing + thresholds.recordTradeOutcome(true); + const stats1 = thresholds.getStats(); + const smoothed1 = stats1.smoothedWinRate; + + thresholds.recordTradeOutcome(false); + const stats2 = thresholds.getStats(); + const smoothed2 = stats2.smoothedWinRate; + + // Smoothed rate should be between the two actual rates + expect(smoothed2).toBeGreaterThan(0); + expect(smoothed2).toBeLessThan(smoothed1); + }); + + it('should maintain sliding window', () => { + const config: Partial = { + recentTradesWindow: 5, + }; + const windowThresholds = new AdaptiveThresholds(config); + + // Record more trades than window size + for (let i = 0; i < 10; i++) { + windowThresholds.recordTradeOutcome(i < 7); // 7 wins, 3 losses + } + + const stats = windowThresholds.getStats(); + expect(stats.recentTrades).toBe(5); // Should cap at window size + }); + }); + + describe('Threshold Adjustments', () => { + it('should require minimum trades before adjusting', () => { + // Record only 5 trades (less than default minimum of 10) + for (let i = 0; i < 5; i++) { + thresholds.recordTradeOutcome(true); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result.minCombinedConfidence).toBe(baseConfig.minCombinedConfidence); + expect(result.adjustmentReason).toContain('Insufficient trades'); + }); + + it('should adjust for high win rate', () => { + // Record 15 winning trades (100% win rate) + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + + // Should lower thresholds (be more aggressive) + expect(result.minCombinedConfidence).toBeLessThan(baseConfig.minCombinedConfidence); + expect(result.minScoreForSignal).toBeLessThan(baseConfig.minScoreForSignal); + expect(result.adjustmentReason).toContain('High win rate'); + }); + + it('should adjust for low win rate', () => { + // Record 3 wins, 12 losses (20% win rate) + for (let i = 0; i < 3; i++) { + thresholds.recordTradeOutcome(true); + } + for (let i = 0; i < 12; i++) { + thresholds.recordTradeOutcome(false); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + + // Should raise thresholds (be more selective) + expect(result.minCombinedConfidence).toBeGreaterThan(baseConfig.minCombinedConfidence); + expect(result.minScoreForSignal).toBeGreaterThan(baseConfig.minScoreForSignal); + expect(result.adjustmentReason).toContain('Low win rate'); + }); + + it('should use base thresholds for target win rate', () => { + // Record 55% win rate (in target range of 50-60%) + // Need to account for EMA smoothing - record more trades + for (let i = 0; i < 20; i++) { + thresholds.recordTradeOutcome(i < 11); // 11 wins, 9 losses = 55% + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result.adjustmentReason).toContain('win rate'); + }); + + it('should apply bounds to prevent extreme adjustments', () => { + const boundedThresholds = new AdaptiveThresholds({ + maxAdjustmentPercent: 0.10, // ±10% max + }); + + // Record 100% win rate to trigger max adjustment + for (let i = 0; i < 15; i++) { + boundedThresholds.recordTradeOutcome(true); + } + + const result = boundedThresholds.getAdjustedThresholds(baseConfig); + + // Adjustment should be bounded + const maxExpected = baseConfig.minCombinedConfidence * 1.1; + const minExpected = baseConfig.minCombinedConfidence * 0.9; + + expect(result.minCombinedConfidence).toBeGreaterThanOrEqual(Math.round(minExpected)); + expect(result.minCombinedConfidence).toBeLessThanOrEqual(Math.round(maxExpected)); + }); + + it('should adjust volume ratio', () => { + // High win rate should lower volume requirement + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const highWinResult = thresholds.getAdjustedThresholds(baseConfig); + expect(highWinResult.minVolumeRatio).toBeLessThanOrEqual(baseConfig.minVolumeRatio); + + // Reset and test low win rate + thresholds.reset(); + for (let i = 0; i < 3; i++) { + thresholds.recordTradeOutcome(true); + } + for (let i = 0; i < 12; i++) { + thresholds.recordTradeOutcome(false); + } + + const lowWinResult = thresholds.getAdjustedThresholds(baseConfig); + expect(lowWinResult.minVolumeRatio).toBeGreaterThanOrEqual(baseConfig.minVolumeRatio); + }); + + it('should adjust direction threshold', () => { + // High win rate should lower direction threshold + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const highWinResult = thresholds.getAdjustedThresholds(baseConfig); + expect(highWinResult.directionThreshold).toBeLessThan(1.10); + + // Reset and test low win rate + thresholds.reset(); + for (let i = 0; i < 3; i++) { + thresholds.recordTradeOutcome(true); + } + for (let i = 0; i < 12; i++) { + thresholds.recordTradeOutcome(false); + } + + const lowWinResult = thresholds.getAdjustedThresholds(baseConfig); + expect(lowWinResult.directionThreshold).toBeGreaterThan(1.10); + }); + + it('should bound volume ratio between 0.1 and 1.0', () => { + const extremeConfig = { + ...baseConfig, + minVolumeRatio: 0.05, + }; + + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const result = thresholds.getAdjustedThresholds(extremeConfig); + expect(result.minVolumeRatio).toBeGreaterThanOrEqual(0.1); + expect(result.minVolumeRatio).toBeLessThanOrEqual(1.0); + }); + }); + + describe('Custom Configuration', () => { + it('should respect custom win rate thresholds', () => { + const customThresholds = new AdaptiveThresholds({ + highWinRateThreshold: 0.70, + lowWinRateThreshold: 0.40, + targetWinRateMin: 0.40, + targetWinRateMax: 0.70, + }); + + // 55% win rate (between custom thresholds) + for (let i = 0; i < 20; i++) { + customThresholds.recordTradeOutcome(i < 11); // 11 wins, 9 losses = 55% + } + + const result = customThresholds.getAdjustedThresholds(baseConfig); + // With custom thresholds, 55% is in target range (0.40-0.70) + expect(result.adjustmentReason).toContain('win rate'); + }); + + it('should respect custom adjustment multipliers', () => { + const customThresholds = new AdaptiveThresholds({ + highWinRateConfidenceMultiplier: 0.85, // 15% reduction + highWinRateScoreMultiplier: 0.80, // 20% reduction + }); + + // 70% win rate + for (let i = 0; i < 15; i++) { + customThresholds.recordTradeOutcome(i < 10.5); + } + + const result = customThresholds.getAdjustedThresholds(baseConfig); + + // Should use custom multipliers + expect(result.minCombinedConfidence).toBeLessThan(baseConfig.minCombinedConfidence); + expect(result.minScoreForSignal).toBeLessThan(baseConfig.minScoreForSignal); + }); + + it('should respect custom smoothing factor', () => { + const highSmoothingThresholds = new AdaptiveThresholds({ + smoothingFactor: 0.8, // High weight for new values + }); + + const lowSmoothingThresholds = new AdaptiveThresholds({ + smoothingFactor: 0.1, // Low weight for new values + }); + + // Record same trades for both + highSmoothingThresholds.recordTradeOutcome(true); + lowSmoothingThresholds.recordTradeOutcome(true); + + const highStats = highSmoothingThresholds.getStats(); + const lowStats = lowSmoothingThresholds.getStats(); + + // High smoothing should be closer to actual win rate + expect(highStats.smoothedWinRate).toBeGreaterThan(lowStats.smoothedWinRate); + }); + }); + + describe('Statistics', () => { + it('should return correct statistics', () => { + for (let i = 0; i < 7; i++) { + thresholds.recordTradeOutcome(true); + } + for (let i = 0; i < 3; i++) { + thresholds.recordTradeOutcome(false); + } + + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(10); + expect(stats.recentWins).toBe(7); + expect(stats.winRate).toBe(0.7); + expect(stats.smoothedWinRate).toBeGreaterThan(0); + expect(stats.canAdjust).toBe(true); + }); + + it('should indicate when adjustment is not possible', () => { + for (let i = 0; i < 5; i++) { + thresholds.recordTradeOutcome(true); + } + + const stats = thresholds.getStats(); + expect(stats.canAdjust).toBe(false); + }); + + it('should handle zero trades', () => { + const stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(0); + expect(stats.winRate).toBe(0); + expect(stats.canAdjust).toBe(false); + }); + }); + + describe('Reset', () => { + it('should reset all statistics', () => { + // Record some trades + for (let i = 0; i < 10; i++) { + thresholds.recordTradeOutcome(i < 7); + } + + let stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(10); + + // Reset + thresholds.reset(); + + stats = thresholds.getStats(); + expect(stats.recentTrades).toBe(0); + expect(stats.recentWins).toBe(0); + expect(stats.winRate).toBe(0); + expect(stats.smoothedWinRate).toBe(0.5); + }); + }); + + describe('Edge Cases', () => { + it('should handle perfect win rate (100%)', () => { + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result).toBeDefined(); + expect(result.adjustmentReason).toContain('High win rate'); + }); + + it('should handle perfect loss rate (0%)', () => { + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(false); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result).toBeDefined(); + expect(result.adjustmentReason).toContain('Low win rate'); + }); + + it('should handle exactly threshold win rates', () => { + // Exactly 60% win rate (high threshold boundary) + for (let i = 0; i < 10; i++) { + thresholds.recordTradeOutcome(i < 6); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result).toBeDefined(); + }); + + it('should handle very small base values', () => { + const smallConfig = { + ...baseConfig, + minCombinedConfidence: 10, + minScoreForSignal: 5, + }; + + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(false); + } + + const result = thresholds.getAdjustedThresholds(smallConfig); + expect(result.minCombinedConfidence).toBeGreaterThan(0); + expect(result.minScoreForSignal).toBeGreaterThan(0); + }); + + it('should handle very large base values', () => { + const largeConfig = { + ...baseConfig, + minCombinedConfidence: 95, + minScoreForSignal: 90, + }; + + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const result = thresholds.getAdjustedThresholds(largeConfig); + expect(result.minCombinedConfidence).toBeGreaterThan(0); + expect(result.minScoreForSignal).toBeGreaterThan(0); + }); + }); + + describe('Win Rate Smoothing', () => { + it('should smooth win rate over time', () => { + const smoothingThresholds = new AdaptiveThresholds({ + smoothingFactor: 0.3, + }); + + // Start with wins + for (let i = 0; i < 5; i++) { + smoothingThresholds.recordTradeOutcome(true); + } + const stats1 = smoothingThresholds.getStats(); + + // Add losses + for (let i = 0; i < 5; i++) { + smoothingThresholds.recordTradeOutcome(false); + } + const stats2 = smoothingThresholds.getStats(); + + // Smoothed rate should change gradually and be between 0 and 1 + expect(stats2.smoothedWinRate).toBeGreaterThan(0); + expect(stats2.smoothedWinRate).toBeLessThan(1); + // Should be different from initial + expect(stats2.smoothedWinRate).not.toBe(stats1.smoothedWinRate); + }); + + it('should converge smoothed win rate to actual over time', () => { + const smoothingThresholds = new AdaptiveThresholds({ + smoothingFactor: 0.3, + recentTradesWindow: 100, // Allow more trades + }); + + // Record many consistent outcomes + for (let i = 0; i < 100; i++) { + smoothingThresholds.recordTradeOutcome(i < 70); // 70% win rate + } + + const stats = smoothingThresholds.getStats(); + + // Smoothed should be reasonably close to actual (within 20%) + expect(Math.abs(stats.smoothedWinRate - 0.7)).toBeLessThan(0.2); + }); + }); + + describe('Adjustment Reasons', () => { + it('should provide clear adjustment reason for high win rate', () => { + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(true); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result.adjustmentReason).toMatch(/High win rate \(\d+\.\d+%\) - being more aggressive/); + }); + + it('should provide clear adjustment reason for low win rate', () => { + for (let i = 0; i < 15; i++) { + thresholds.recordTradeOutcome(false); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + expect(result.adjustmentReason).toMatch(/Low win rate \(\d+\.\d+%\) - being more selective/); + }); + + it('should provide clear adjustment reason for target range', () => { + // Record 20 trades with 55% win rate to stabilize EMA + for (let i = 0; i < 20; i++) { + thresholds.recordTradeOutcome(i < 11); + } + + const result = thresholds.getAdjustedThresholds(baseConfig); + // Just check that it provides a reason mentioning win rate + expect(result.adjustmentReason).toMatch(/\d+\.\d+%/); + expect(result.adjustmentReason.toLowerCase()).toContain('win rate'); + }); + }); +}); diff --git a/tests/unit/memory/contextual-memory.test.ts b/tests/unit/memory/contextual-memory.test.ts index b1b334a..807b1eb 100644 --- a/tests/unit/memory/contextual-memory.test.ts +++ b/tests/unit/memory/contextual-memory.test.ts @@ -99,5 +99,216 @@ describe('ContextualMemory', () => { expect(boost.confidenceBoost).toBe(0); }); }); + + describe('Market Event Detection', () => { + it('should detect volatility spike', () => { + const highVolatilityIndicators = { + ...mockIndicators, + atrPercent: 5.0, // High volatility + }; + + contextualMemory.storeContext('BTCUSDT', highVolatilityIndicators, []); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost).toBeDefined(); + }); + + it('should detect volume surge', () => { + const highVolumeIndicators = { + ...mockIndicators, + volumeRatio: 3.0, // High volume + }; + + contextualMemory.storeContext('BTCUSDT', highVolumeIndicators, []); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost).toBeDefined(); + }); + + it('should detect trend reversal', () => { + // Store uptrend context + contextualMemory.storeContext('BTCUSDT', mockIndicators, []); + + // Store downtrend context (reversal) + const reversalIndicators = { + ...mockIndicators, + trend: 'DOWN' as const, + momentum: -0.5, + }; + + contextualMemory.storeContext('BTCUSDT', reversalIndicators, []); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'SHORT', 75); + expect(boost).toBeDefined(); + }); + }); + + describe('Context Expiration', () => { + it('should handle multiple contexts for same symbol', () => { + // Store multiple contexts + for (let i = 0; i < 5; i++) { + contextualMemory.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 75, outcome: 'win' }, + ]); + } + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost.confidenceBoost).toBeGreaterThan(0); + }); + + it('should maintain separate contexts for different symbols', () => { + contextualMemory.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 75, outcome: 'win' }, + ]); + + contextualMemory.storeContext('ETHUSDT', mockIndicators, [ + { type: 'SHORT', confidence: 75, outcome: 'loss' }, + ]); + + const btcBoost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + const ethBoost = contextualMemory.getContextualBoost('ETHUSDT', 'SHORT', 75); + + expect(btcBoost.confidenceBoost).toBeGreaterThan(0); + expect(ethBoost.confidenceBoost).toBeLessThan(0); + }); + }); + + describe('Trade Streak Analysis', () => { + it('should boost confidence on winning streak', () => { + // Store winning signals + const winningSignals = Array(5).fill(null).map(() => ({ + type: 'LONG' as SignalType, + confidence: 75, + outcome: 'win' as const, + })); + + contextualMemory.storeContext('BTCUSDT', mockIndicators, winningSignals); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost.confidenceBoost).toBeGreaterThan(0); + }); + + it('should penalize confidence on losing streak', () => { + // Store losing signals + const losingSignals = Array(5).fill(null).map(() => ({ + type: 'LONG' as SignalType, + confidence: 75, + outcome: 'loss' as const, + })); + + contextualMemory.storeContext('BTCUSDT', mockIndicators, losingSignals); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost.confidenceBoost).toBeLessThan(0); + }); + + it('should handle mixed win/loss patterns', () => { + const mixedSignals = [ + { type: 'LONG' as SignalType, confidence: 75, outcome: 'win' as const }, + { type: 'LONG' as SignalType, confidence: 75, outcome: 'loss' as const }, + { type: 'LONG' as SignalType, confidence: 75, outcome: 'win' as const }, + ]; + + contextualMemory.storeContext('BTCUSDT', mockIndicators, mixedSignals); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost).toBeDefined(); + }); + }); + + describe('Signal Pattern Analysis', () => { + it('should identify winning patterns for signal type', () => { + const signals = [ + { type: 'LONG' as SignalType, confidence: 75, outcome: 'win' as const }, + { type: 'LONG' as SignalType, confidence: 75, outcome: 'win' as const }, + { type: 'SHORT' as SignalType, confidence: 75, outcome: 'loss' as const }, + ]; + + contextualMemory.storeContext('BTCUSDT', mockIndicators, signals); + + const longBoost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + const shortBoost = contextualMemory.getContextualBoost('BTCUSDT', 'SHORT', 75); + + expect(longBoost.confidenceBoost).toBeGreaterThan(0); + expect(shortBoost.confidenceBoost).toBeLessThan(0); + }); + + it('should handle signals without outcomes', () => { + const signals = [ + { type: 'LONG' as SignalType, confidence: 75 }, + { type: 'SHORT' as SignalType, confidence: 75 }, + ]; + + contextualMemory.storeContext('BTCUSDT', mockIndicators, signals); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost).toBeDefined(); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty signal history', () => { + contextualMemory.storeContext('BTCUSDT', mockIndicators, []); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost).toBeDefined(); + }); + + it('should handle unknown signal types', () => { + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'UNKNOWN' as SignalType, 75); + expect(boost.confidenceBoost).toBe(0); + }); + + it('should handle very high confidence values', () => { + contextualMemory.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 95, outcome: 'win' }, + ]); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 95); + expect(boost).toBeDefined(); + }); + + it('should handle very low confidence values', () => { + contextualMemory.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 10, outcome: 'win' }, + ]); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 10); + expect(boost).toBeDefined(); + }); + + it('should handle extreme price movements', () => { + const extremeIndicators = { + ...mockIndicators, + price: 100000, + momentum: 5.0, + volatility: 10.0, + }; + + contextualMemory.storeContext('BTCUSDT', extremeIndicators, []); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + expect(boost).toBeDefined(); + }); + }); + + describe('Boost Bounds', () => { + it('should provide reasonable boost values', () => { + // Store many winning trades + const signals = Array(20).fill(null).map(() => ({ + type: 'LONG' as SignalType, + confidence: 75, + outcome: 'win' as const, + })); + + contextualMemory.storeContext('BTCUSDT', mockIndicators, signals); + + const boost = contextualMemory.getContextualBoost('BTCUSDT', 'LONG', 75); + + // Boosts should be bounded to prevent extreme values + expect(Math.abs(boost.confidenceBoost)).toBeLessThan(50); + expect(Math.abs(boost.scoreBoost)).toBeLessThan(50); + }); + }); }); diff --git a/tests/unit/memory/memory-integration.test.ts b/tests/unit/memory/memory-integration.test.ts new file mode 100644 index 0000000..9f346da --- /dev/null +++ b/tests/unit/memory/memory-integration.test.ts @@ -0,0 +1,469 @@ +/** + * Memory System Integration Tests + * Tests the full memory flow from trade storage to retrieval and learning + */ + +import { MemoryManager, type MemoryManagerConfig } from '../../../src/services/memory/memory-manager'; +import type { Position, Trade, TechnicalIndicators } from '../../../src/types'; + +describe('Memory System Integration', () => { + let manager: MemoryManager; + let config: MemoryManagerConfig; + let mockPosition: Position; + let mockTrade: Trade; + let mockIndicators: TechnicalIndicators; + let mockSignal: { confidence: number; score: number; reasons: string[]; llmAgreed: boolean }; + + beforeEach(() => { + config = { + tradeHistory: { + maxTradesInMemory: 100, + persistenceEnabled: false, + }, + persistence: { + basePath: '/tmp/test-memory-integration', + enabled: false, + autoSaveInterval: 60000, + version: '1.0.0', + }, + enabled: true, + }; + + manager = new MemoryManager(config); + + mockIndicators = { + price: 100, + rsi: 50, + momentum: 0.5, + volumeRatio: 1.2, + trend: 'UP', + ema9: 99, + ema21: 98, + ema50: 97, + macd: 0.1, + macdSignal: 0.05, + macdHistogram: 0.05, + macdCrossUp: true, + macdCrossDown: false, + bbUpper: 105, + bbMiddle: 100, + bbLower: 95, + bbPercentB: 0.5, + stochK: 50, + stochD: 50, + roc: 0.5, + williamsR: -50, + atr: 2, + atrPercent: 2, + divergence: undefined, + }; + + const now = Date.now(); + mockPosition = { + id: 'pos-1', + agentId: 'agent-1', + userId: 'user-1', + symbol: 'BTCUSDT', + side: 'long', + size: 0.1, + entryPrice: 100, + currentPrice: 105, + leverage: 10, + marginUsed: 1, + unrealizedPnl: 0.5, + unrealizedROE: 50, + highestROE: 50, + lowestROE: 0, + openedAt: now, + updatedAt: now + 1000, + stopLoss: 95, + takeProfit: 110, + } as Position; + + mockTrade = { + id: 'trade-1', + positionId: 'pos-1', + agentId: 'agent-1', + userId: 'user-1', + symbol: 'BTCUSDT', + side: 'sell', + type: 'close', + quantity: 0.1, + price: 105, + realizedPnl: 50, + fees: 0.5, + reason: 'Take profit', + executedAt: now + 1000, + }; + + mockSignal = { + confidence: 70, + score: 75, + reasons: ['RSI oversold', 'MACD cross up', 'Volume surge'], + llmAgreed: true, + }; + }); + + describe('End-to-End Trade Flow', () => { + it('should store trade and update all subsystems', async () => { + // Store a trade + await manager.storeTrade( + mockPosition, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + + // Verify trade history + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(1); + expect(stats.wins).toBe(1); + + // Verify pattern learning + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(patternBoost).toBeDefined(); + + // Verify symbol intelligence + const priority = manager.getSymbolPriority('BTCUSDT', ['trend:UP']); + expect(priority).toBeGreaterThan(0); + + // Verify regime detection + const regime = manager.getRegimeAdjustments(mockIndicators); + expect(regime).toBeDefined(); + }); + + it('should learn from multiple trades and adapt', async () => { + // Store 20 trades with consistent patterns + for (let i = 0; i < 20; i++) { + const isWin = i % 3 !== 0; // ~67% win rate + const trade = { + ...mockTrade, + id: `trade-${i}`, + realizedPnl: isWin ? 50 : -20, + }; + + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + trade, + mockIndicators, + mockSignal, + isWin ? 'Take profit' : 'Stop loss' + ); + } + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(20); + expect(stats.winRate).toBeCloseTo(0.67, 1); + + // Pattern learner should provide boosts + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(patternBoost).toBeDefined(); + + // Adaptive filters should be active + const filters = manager.getAdaptiveFilters(); + expect(filters).toBeDefined(); + + // Adaptive thresholds should be active + const thresholds = manager.getAdaptiveThresholds({ + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + }); + expect(thresholds).toBeDefined(); + }); + }); + + describe('Multi-Symbol Learning', () => { + it('should learn different patterns for different symbols', async () => { + // BTCUSDT - Winning pattern + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + { ...mockPosition, symbol: 'BTCUSDT', id: `btc-pos-${i}` }, + { ...mockTrade, id: `btc-trade-${i}`, realizedPnl: 50 }, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + // ETHUSDT - Losing pattern + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + { ...mockPosition, symbol: 'ETHUSDT', id: `eth-pos-${i}` }, + { ...mockTrade, id: `eth-trade-${i}`, realizedPnl: -20 }, + mockIndicators, + mockSignal, + 'Stop loss' + ); + } + + // BTC should have higher priority + const btcPriority = manager.getSymbolPriority('BTCUSDT', ['trend:UP']); + const ethPriority = manager.getSymbolPriority('ETHUSDT', ['trend:UP']); + + expect(btcPriority).toBeGreaterThan(ethPriority); + + // Ranked symbols should reflect this + const ranked = manager.getRankedSymbols(['trend:UP']); + expect(ranked.indexOf('BTCUSDT')).toBeLessThan(ranked.indexOf('ETHUSDT')); + }); + }); + + describe('Market Regime Adaptation', () => { + it('should adapt to changing market regimes', async () => { + // Store trades in uptrend + const uptrendIndicators = { ...mockIndicators, trend: 'UP' as const, momentum: 0.8 }; + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + { ...mockPosition, id: `up-pos-${i}` }, + { ...mockTrade, id: `up-trade-${i}`, realizedPnl: 50 }, + uptrendIndicators, + mockSignal, + 'Take profit' + ); + } + + const upRegime = manager.getRegimeAdjustments(uptrendIndicators); + + // Store trades in downtrend + const downtrendIndicators = { ...mockIndicators, trend: 'DOWN' as const, momentum: -0.8 }; + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + { ...mockPosition, id: `down-pos-${i}`, side: 'short' }, + { ...mockTrade, id: `down-trade-${i}`, realizedPnl: -20 }, + downtrendIndicators, + { ...mockSignal, reasons: ['RSI overbought'] }, + 'Stop loss' + ); + } + + const downRegime = manager.getRegimeAdjustments(downtrendIndicators); + + // Regimes should differ + expect(upRegime).toBeDefined(); + expect(downRegime).toBeDefined(); + }); + }); + + describe('Contextual Memory Integration', () => { + it('should store context and use it for boosting', async () => { + // Store context + manager.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 70, outcome: 'win' }, + ]); + + // Complete a trade + await manager.storeTrade( + mockPosition, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + + // Update signal outcome + manager.updateSignalOutcome('BTCUSDT', 'LONG', 'win'); + + // Get boost + const boost = manager.getContextualBoost('BTCUSDT', 'LONG', 70); + expect(boost.confidenceBoost).toBeGreaterThan(0); + }); + }); + + describe('Adaptive System Coordination', () => { + it('should coordinate adaptive filters and thresholds', async () => { + // Record losing streak to trigger adaptations + for (let i = 0; i < 15; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}`, realizedPnl: -20 }, + mockIndicators, + mockSignal, + 'Stop loss' + ); + } + + const filters = manager.getAdaptiveFilters(); + const thresholds = manager.getAdaptiveThresholds({ + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + }); + + // Both should be more restrictive + expect(filters).toBeDefined(); + expect(thresholds).toBeDefined(); + expect(thresholds?.minCombinedConfidence).toBeGreaterThan(70); + }); + + it('should coordinate pattern learning and regime detection', async () => { + // Store trades with consistent patterns in trending market + for (let i = 0; i < 10; i++) { + await manager.storeTrade( + mockPosition, + { ...mockTrade, id: `trade-${i}`, realizedPnl: 50 }, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + // Both systems should contribute to signal evaluation + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + const regimeAdj = manager.getRegimeAdjustments(mockIndicators); + + expect(patternBoost.confidenceBoost).toBeDefined(); + expect(regimeAdj.confidenceMultiplier).toBeDefined(); + }); + }); + + describe('Performance Under Load', () => { + it('should handle many trades efficiently', async () => { + const startTime = Date.now(); + + // Store 100 trades + for (let i = 0; i < 100; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}` }, + mockIndicators, + mockSignal, + 'Exit' + ); + } + + const endTime = Date.now(); + const duration = endTime - startTime; + + // Should complete in reasonable time (less than 5 seconds) + expect(duration).toBeLessThan(5000); + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(100); + }); + }); + + describe('Memory Persistence Coordination', () => { + it('should auto-save after 10 trades', async () => { + // Store 10 trades to trigger auto-save + for (let i = 0; i < 10; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}` }, + mockIndicators, + mockSignal, + 'Exit' + ); + } + + // Should not throw errors during auto-save + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(10); + }); + + it('should initialize from persisted data', async () => { + // Store some trades + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}` }, + mockIndicators, + mockSignal, + 'Exit' + ); + } + + // Initialize should not throw + await expect(manager.initialize()).resolves.not.toThrow(); + }); + }); + + describe('Signal Enhancement Flow', () => { + it('should enhance signals through full memory pipeline', async () => { + // Build up history + for (let i = 0; i < 15; i++) { + await manager.storeTrade( + mockPosition, + { ...mockTrade, id: `trade-${i}`, realizedPnl: 50 }, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + // Get all enhancements for a new signal + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + const contextBoost = manager.getContextualBoost('BTCUSDT', 'LONG', 70); + const regimeAdj = manager.getRegimeAdjustments(mockIndicators); + const filters = manager.getAdaptiveFilters(); + const thresholds = manager.getAdaptiveThresholds({ + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + }); + + // All systems should contribute + expect(patternBoost).toBeDefined(); + expect(contextBoost).toBeDefined(); + expect(regimeAdj).toBeDefined(); + expect(filters).toBeDefined(); + expect(thresholds).toBeDefined(); + + // Combined effect should be positive for winning pattern + const totalConfidenceBoost = patternBoost.confidenceBoost + contextBoost.confidenceBoost; + expect(totalConfidenceBoost).toBeGreaterThan(0); + }); + }); + + describe('Edge Cases in Integration', () => { + it('should handle rapid trade succession', async () => { + // Store many trades quickly + const promises = []; + for (let i = 0; i < 20; i++) { + promises.push( + manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}` }, + mockIndicators, + mockSignal, + 'Exit' + ) + ); + } + + await Promise.all(promises); + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(20); + }); + + it('should handle mixed winning and losing patterns', async () => { + // Alternate wins and losses + for (let i = 0; i < 20; i++) { + const isWin = i % 2 === 0; + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { + ...mockTrade, + id: `trade-${i}`, + realizedPnl: isWin ? 50 : -20, + }, + mockIndicators, + mockSignal, + isWin ? 'Take profit' : 'Stop loss' + ); + } + + const stats = manager.getStatistics(); + expect(stats.winRate).toBe(0.5); + + // All systems should still function + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(patternBoost).toBeDefined(); + }); + }); +}); diff --git a/tests/unit/memory/memory-manager.test.ts b/tests/unit/memory/memory-manager.test.ts new file mode 100644 index 0000000..5e9cd32 --- /dev/null +++ b/tests/unit/memory/memory-manager.test.ts @@ -0,0 +1,545 @@ +/** + * Memory Manager Unit Tests + */ + +import { MemoryManager, type MemoryManagerConfig } from '../../../src/services/memory/memory-manager'; +import type { Position, Trade, TechnicalIndicators } from '../../../src/types'; + +describe('MemoryManager', () => { + let manager: MemoryManager; + let config: MemoryManagerConfig; + let mockPosition: Position; + let mockTrade: Trade; + let mockIndicators: TechnicalIndicators; + let mockSignal: { confidence: number; score: number; reasons: string[]; llmAgreed: boolean }; + + beforeEach(() => { + config = { + tradeHistory: { + maxTradesInMemory: 100, + persistenceEnabled: false, + }, + persistence: { + basePath: '/tmp/test-memory', + enabled: false, + autoSaveInterval: 60000, + version: '1.0.0', + }, + enabled: true, + }; + + manager = new MemoryManager(config); + + mockIndicators = { + price: 100, + rsi: 50, + momentum: 0.5, + volumeRatio: 1.2, + trend: 'UP', + ema9: 99, + ema21: 98, + ema50: 97, + macd: 0.1, + macdSignal: 0.05, + macdHistogram: 0.05, + macdCrossUp: true, + macdCrossDown: false, + bbUpper: 105, + bbMiddle: 100, + bbLower: 95, + bbPercentB: 0.5, + stochK: 50, + stochD: 50, + roc: 0.5, + williamsR: -50, + atr: 2, + atrPercent: 2, + divergence: undefined, + }; + + const now = Date.now(); + mockPosition = { + id: 'pos-1', + agentId: 'agent-1', + userId: 'user-1', + symbol: 'BTCUSDT', + side: 'long', + size: 0.1, + entryPrice: 100, + currentPrice: 105, + leverage: 10, + marginUsed: 1, + unrealizedPnl: 0.5, + unrealizedROE: 50, + highestROE: 50, + lowestROE: 0, + openedAt: now, + updatedAt: now + 1000, + stopLoss: 95, + takeProfit: 110, + } as Position; + + mockTrade = { + id: 'trade-1', + positionId: 'pos-1', + agentId: 'agent-1', + userId: 'user-1', + symbol: 'BTCUSDT', + side: 'sell', + type: 'close', + quantity: 0.1, + price: 105, + realizedPnl: 50, + fees: 0.5, + reason: 'Take profit', + executedAt: now + 1000, + }; + + mockSignal = { + confidence: 70, + score: 75, + reasons: ['RSI oversold', 'MACD cross up', 'Volume surge'], + llmAgreed: true, + }; + }); + + describe('Constructor', () => { + it('should initialize all memory subsystems when enabled', () => { + const enabledManager = new MemoryManager({ + ...config, + enabled: true, + }); + + expect(enabledManager).toBeDefined(); + const stats = enabledManager.getStatistics(); + expect(stats).toBeDefined(); + }); + + it('should initialize with disabled state', () => { + const disabledManager = new MemoryManager({ + ...config, + enabled: false, + }); + + expect(disabledManager).toBeDefined(); + const stats = disabledManager.getStatistics(); + expect(stats.totalTrades).toBe(0); + }); + }); + + describe('Disabled Mode', () => { + beforeEach(() => { + manager = new MemoryManager({ + ...config, + enabled: false, + }); + }); + + it('should return default statistics when disabled', () => { + const stats = manager.getStatistics(); + expect(stats).toEqual({ + totalTrades: 0, + wins: 0, + losses: 0, + breakevens: 0, + winRate: 0, + avgROE: 0, + totalPnL: 0, + }); + }); + + it('should return zero boosts when disabled', () => { + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(patternBoost).toEqual({ + confidenceBoost: 0, + scoreBoost: 0, + reason: 'Memory disabled', + }); + + const contextBoost = manager.getContextualBoost('BTCUSDT', 'LONG', 70); + expect(contextBoost).toEqual({ + confidenceBoost: 0, + scoreBoost: 0, + reason: 'Memory disabled', + }); + }); + + it('should return default filters when disabled', () => { + const filters = manager.getAdaptiveFilters(); + expect(filters).toEqual({ + minCombinedConfidence: 55, + minScoreForSignal: 50, + requireTrendAlignment: false, + requireVolumeConfirmation: false, + requireMultiIndicatorConfluence: false, + minIndicatorConfluence: 3, + minVolumeSpike: 0.3, + minTrendStrength: 0.3, + }); + }); + + it('should return null for adaptive thresholds when disabled', () => { + const thresholds = manager.getAdaptiveThresholds({ + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + }); + expect(thresholds).toBeNull(); + }); + + it('should return default regime adjustments when disabled', () => { + const adjustments = manager.getRegimeAdjustments(mockIndicators); + expect(adjustments).toEqual({ + confidenceMultiplier: 1.0, + positionSizeMultiplier: 1.0, + stopLossMultiplier: 1.0, + takeProfitMultiplier: 1.0, + }); + }); + + it('should return default symbol priority when disabled', () => { + const priority = manager.getSymbolPriority('BTCUSDT', ['trend:UP']); + expect(priority).toBe(0.5); + }); + + it('should return empty ranked symbols when disabled', () => { + const ranked = manager.getRankedSymbols(['trend:UP']); + expect(ranked).toEqual([]); + }); + + it('should not throw when storing trades while disabled', async () => { + await expect( + manager.storeTrade(mockPosition, mockTrade, mockIndicators, mockSignal, 'Take profit') + ).resolves.not.toThrow(); + }); + + it('should not throw when storing context while disabled', () => { + expect(() => { + manager.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 70, outcome: 'win' }, + ]); + }).not.toThrow(); + }); + }); + + describe('Trade Storage', () => { + it('should store a trade successfully', async () => { + await manager.storeTrade( + mockPosition, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(1); + expect(stats.wins).toBe(1); + expect(stats.totalPnL).toBe(50); + }); + + it('should handle multiple trades', async () => { + for (let i = 0; i < 5; i++) { + const trade = { + ...mockTrade, + id: `trade-${i}`, + realizedPnl: i % 2 === 0 ? 50 : -20, + realizedROE: i % 2 === 0 ? 50 : -20, + }; + + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + trade, + mockIndicators, + mockSignal, + 'Exit' + ); + } + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(5); + expect(stats.wins).toBe(3); + expect(stats.losses).toBe(2); + }); + + it('should update all subsystems when storing a trade', async () => { + await manager.storeTrade( + mockPosition, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + + // Pattern learner should have learned + const patternBoost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(patternBoost).toBeDefined(); + + // Symbol intelligence should have data + const priority = manager.getSymbolPriority('BTCUSDT', ['trend:UP']); + expect(priority).toBeGreaterThan(0); + }); + + it('should trigger auto-save after 10 trades', async () => { + for (let i = 0; i < 10; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}` }, + mockIndicators, + mockSignal, + 'Exit' + ); + } + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(10); + }); + }); + + describe('Pattern Boosting', () => { + it('should return pattern boost for signal', () => { + const boost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(boost).toHaveProperty('confidenceBoost'); + expect(boost).toHaveProperty('scoreBoost'); + expect(boost).toHaveProperty('reason'); + }); + + it('should use market regime for pattern boost', async () => { + // Store some winning trades to establish patterns + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + mockPosition, + { ...mockTrade, id: `trade-${i}`, realizedPnl: 50 }, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + const boost = manager.getPatternBoost(mockIndicators, 70, 'BTCUSDT', true, 12); + expect(boost).toBeDefined(); + }); + }); + + describe('Contextual Boosting', () => { + it('should return contextual boost', () => { + const boost = manager.getContextualBoost('BTCUSDT', 'LONG', 70); + expect(boost).toHaveProperty('confidenceBoost'); + expect(boost).toHaveProperty('scoreBoost'); + expect(boost).toHaveProperty('reason'); + }); + + it('should store context', () => { + expect(() => { + manager.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 70, outcome: 'win' }, + ]); + }).not.toThrow(); + }); + + it('should update signal outcome', () => { + manager.storeContext('BTCUSDT', mockIndicators, [ + { type: 'LONG', confidence: 70 }, + ]); + + expect(() => { + manager.updateSignalOutcome('BTCUSDT', 'LONG', 'win'); + }).not.toThrow(); + }); + }); + + describe('Adaptive Filters', () => { + it('should return adaptive filters', () => { + const filters = manager.getAdaptiveFilters(); + expect(filters).toHaveProperty('minCombinedConfidence'); + expect(filters).toHaveProperty('minScoreForSignal'); + expect(filters).toHaveProperty('requireTrendAlignment'); + }); + + it('should adjust filters based on trade outcomes', async () => { + const initialFilters = manager.getAdaptiveFilters(); + + // Record some losing trades + for (let i = 0; i < 10; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}`, realizedPnl: -20 }, + mockIndicators, + mockSignal, + 'Stop loss' + ); + } + + const adjustedFilters = manager.getAdaptiveFilters(); + expect(adjustedFilters).toBeDefined(); + }); + }); + + describe('Adaptive Thresholds', () => { + it('should return adaptive thresholds', () => { + const baseConfig = { + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + }; + + const thresholds = manager.getAdaptiveThresholds(baseConfig); + expect(thresholds).toBeDefined(); + }); + + it('should return threshold statistics', () => { + const stats = manager.getAdaptiveThresholdStats(); + expect(stats).toHaveProperty('recentTrades'); + expect(stats).toHaveProperty('winRate'); + expect(stats).toHaveProperty('smoothedWinRate'); + }); + + it('should adjust thresholds based on win rate', async () => { + const baseConfig = { + minCombinedConfidence: 70, + minConfidenceWithoutLLM: 75, + minScoreForSignal: 60, + minVolumeRatio: 0.8, + }; + + // Record winning trades + for (let i = 0; i < 15; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}`, realizedPnl: 50 }, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + const thresholds = manager.getAdaptiveThresholds(baseConfig); + expect(thresholds).toBeDefined(); + expect(thresholds?.adjustmentReason).toBeDefined(); + }); + }); + + describe('Market Regime', () => { + it('should return regime adjustments', () => { + const adjustments = manager.getRegimeAdjustments(mockIndicators); + expect(adjustments).toHaveProperty('confidenceMultiplier'); + expect(adjustments).toHaveProperty('positionSizeMultiplier'); + expect(adjustments).toHaveProperty('stopLossMultiplier'); + expect(adjustments).toHaveProperty('takeProfitMultiplier'); + }); + + it('should update regime based on indicators', async () => { + const initialAdjustments = manager.getRegimeAdjustments(mockIndicators); + + // Store trades and check regime updates + for (let i = 0; i < 5; i++) { + await manager.storeTrade( + mockPosition, + { ...mockTrade, id: `trade-${i}` }, + mockIndicators, + mockSignal, + 'Exit' + ); + } + + const updatedAdjustments = manager.getRegimeAdjustments({ + ...mockIndicators, + trend: 'DOWN', + momentum: -0.5, + }); + + expect(updatedAdjustments).toBeDefined(); + }); + }); + + describe('Symbol Intelligence', () => { + it('should return symbol priority', async () => { + await manager.storeTrade( + mockPosition, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + + const priority = manager.getSymbolPriority('BTCUSDT', ['trend:UP']); + expect(priority).toBeGreaterThanOrEqual(0); + expect(priority).toBeLessThanOrEqual(1); + }); + + it('should rank symbols', async () => { + // Store trades for multiple symbols + for (const symbol of ['BTCUSDT', 'ETHUSDT', 'SOLUSDT']) { + await manager.storeTrade( + { ...mockPosition, symbol }, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + const ranked = manager.getRankedSymbols(['trend:UP']); + expect(Array.isArray(ranked)).toBe(true); + }); + }); + + describe('Memory Persistence', () => { + it('should save memory without errors', async () => { + await manager.storeTrade( + mockPosition, + mockTrade, + mockIndicators, + mockSignal, + 'Take profit' + ); + + await expect(manager.saveMemory()).resolves.not.toThrow(); + }); + + it('should handle initialization', async () => { + await expect(manager.initialize()).resolves.not.toThrow(); + }); + }); + + describe('Statistics', () => { + it('should return accurate statistics', async () => { + // Store winning trades + for (let i = 0; i < 7; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}`, realizedPnl: 50 }, + mockIndicators, + mockSignal, + 'Take profit' + ); + } + + // Store losing trades + for (let i = 7; i < 10; i++) { + await manager.storeTrade( + { ...mockPosition, id: `pos-${i}` }, + { ...mockTrade, id: `trade-${i}`, realizedPnl: -20 }, + mockIndicators, + mockSignal, + 'Stop loss' + ); + } + + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(10); + expect(stats.wins).toBe(7); + expect(stats.losses).toBe(3); + expect(stats.winRate).toBeCloseTo(0.7, 1); + expect(stats.totalPnL).toBeCloseTo(290, 0); + }); + + it('should handle zero trades', () => { + const stats = manager.getStatistics(); + expect(stats.totalTrades).toBe(0); + expect(stats.winRate).toBe(0); + }); + }); +});