import { EntryTriggerRule } from '../src/strategies/rules/EntryTriggerRule.js'; import { ProStrategyEngine } from '../src/strategies/ProStrategyEngine.js'; import { IExchangeConnector, Candle } from '../src/connectors/types.js'; import { config } from '../src/config/index.js'; import logger from '../src/utils/logger.js'; // --- MOCK EXCHANGE --- class MockExchange implements IExchangeConnector { private candles4h: Candle[]; private candles1h: Candle[]; private candles15m: Candle[]; // Added for 15m specific data constructor(data: { '4h': Candle[], '1h': Candle[], '15m'?: Candle[] }) { this.candles4h = data['4h']; this.candles1h = data['1h']; this.candles15m = data['15m'] || []; // Initialize 15m, default to empty array if not provided } async fetchOHLCV(symbol: string, timeframe: string, since?: number): Promise { if (timeframe === '4h') return this.candles4h; if (timeframe === '1h') return this.candles1h; if (timeframe === '15m') return this.candles15m.length > 0 ? this.candles15m : this.candles1h; // Mock 15m with 1h data if no specific 15m data is provided return []; } async placeOrder(symbol: string, side: 'buy' | 'sell', qty: number, type: 'market' | 'limit'): Promise { return { id: 'mock-order' }; } async getPosition(symbol: string): Promise { return null; } } // --- HELPER TO GENERATE CANDLES --- function generateTrend(length: number, startPrice: number, trend: 'up' | 'down' | 'flat', noise: number = 0.001): Candle[] { const candles: Candle[] = []; let price = startPrice; const now = Date.now(); for (let i = 0; i < length; i++) { let change = 0; if (trend === 'up') change = price * 0.0005; // +0.05% per candle (Steady trend, not parabolic) if (trend === 'down') change = -price * 0.0005; // Add minimal noise price += change + (Math.random() - 0.5) * price * noise; candles.push({ timestamp: now - (length - i) * 60 * 60 * 1000, open: price, high: price * 1.001, low: price * 0.999, close: price, volume: 1000 }); } return candles; } // --- TEST RUNNER --- async function runTest() { logger.info('--- STARTING PRO STRATEGY VERIFICATION ---'); // SCENARIO 1: STRONG UPTREND (Expect BUY) // 4H: Uptrend // 1H: Uptrend + Pullback? // Session: Force Session window to active // Mock Session & RSI Limits for Test const currentHour = new Date().getUTCHours(); config.PRO_STRATEGY.PARAMETERS.SESSION_WINDOWS = [{ start: 0, end: 24 }]; config.PRO_STRATEGY.PARAMETERS.RSI_OVERBOUGHT = 101; // Allow strong trend for test case const candles4h_up = generateTrend(300, 50000, 'up'); // 50000 -> 80000 const candles1h_up = generateTrend(100, 80000, 'up'); // Inject Pullback in last few candles of 1H to satisfy ZoneRule if purely momentum // Actually ZoneRule says "Price <= EMA20 * 1.5%". // MomentumRule says "RSI > 50". // Creating Mock Exchange const mockExchange = new MockExchange({ '4h': candles4h_up, '1h': candles1h_up }); const engine = new ProStrategyEngine(mockExchange); const result = await engine.execute('BTC/USD'); logger.info(`\n[TEST 1] UPTREND SCENARIO RESULT:`); if (result?.passed) { logger.info(`✅ PASSED -> Signal: ${result.signal}`); logger.info(`\n--- DETAILED RULE BREAKDOWN ---`); logger.info(result.reason); logger.info(`-------------------------------\n`); } else { logger.error(`❌ FAILED -> ${result?.ruleName}: ${result?.reason}`); } // SCENARIO 2: MOMENTUM MISMATCH (Expect FAIL) // 4H: Uptrend // 1H: Sharp Drop (Bearish Momentum) const candles1h_crash = generateTrend(100, 80000, 'down'); const mockExchange2 = new MockExchange({ '4h': candles4h_up, // 4H says UP '1h': candles1h_crash // 1H says DOWN }); const engine2 = new ProStrategyEngine(mockExchange2); const resultConflict = await engine2.execute('BTC/USD'); logger.info(`\n[TEST 2] CONFLICT SCENARIO RESULT:`); if (resultConflict?.passed) { logger.error(`❌ FAILED -> Should have skipped due to conflict!`); } else { logger.info(`✅ CORRECTLY FAILED -> ${resultConflict?.ruleName}: ${resultConflict?.reason}`); } // --- TEST 3: ENTRY TRIGGER SCENARIO --- logger.info(`\n[TEST 3] ENTRY TRIGGER SCENARIO: EMA RECLAIM`); // 1. Uptrend Base const candlesReclaim = generateTrend(100, 80000, 'up'); const len = candlesReclaim.length; // 2. Manipulate candles for Reclaim (Dip -> Reclaim) // We strictly test CLOSED candles (Engine slices off the last one) // So we need to modify [len-3] (Prev Closed) and [len-2] (Last Closed) candlesReclaim[len - 3].close = 79000; // Dip below EMA (~83k) candlesReclaim[len - 2].close = 85000; // Reclaim above EMA // Build Context // Now expects (4h, 1h, 15m) // For this test, we can just pass the "Trend" candles as 15m instructions too if we wanted, // or just pass dummy data for 15m if the rule we are testing (EntryTrigger) uses the "Execution" timeframe using 1h for now? // Wait, we haven't updated the rules yet. They probably still use 1h explicitly. // Ideally we pass valid 15m data. Let's reuse the Reclaim candles for 15m slot if we are testing the reclaim logic. const contextReclaim = engine['buildContext'](candles4h_up, candlesReclaim, candlesReclaim); // Force Context Price to match last closed candle contextReclaim.currentPrice = 85000; // Verify Trigger const resultReclaim = await new EntryTriggerRule().check(contextReclaim); if (resultReclaim.passed) { logger.info(`✅ PASSED -> Signal: ${resultReclaim.signal} | ${resultReclaim.reason}`); } else { // Debug info logger.error(`❌ FAILED -> ${resultReclaim.reason}`); logger.info(`Debug: Prev Close: ${candlesReclaim[len - 2].close}, Curr Close: ${candlesReclaim[len - 1].close}, EMA: ${contextReclaim.ema20_1h}`); } // --- TEST 4: SCALPING 15m VERIFICATION --- logger.info(`\n[TEST 4] SCALPING 15m VERIFICATION`); // Switch to 15m Execution config.PRO_STRATEGY.PARAMETERS.EXECUTION_TIMEFRAME = '15m'; // Scenario: // 1H is Bearish (RSI < 50) -> If we used 1H, Momentum would Fail. // 15m is Bullish (RSI > 50) -> If we use 15m, Momentum should Pass. // Generate 1H Bearish Trend const candles1h_bearish = generateTrend(100, 80000, 'down'); // RSI likely < 30 or low // Generate 15m Bullish Trend (Rally) const candles15m_bullish = generateTrend(100, 80000, 'up'); // RSI likely > 70 // Need to ensure 15m RSI is healthy (e.g. 60), not Overbought (>70 is default overbought check?) // generateTrend 'up' usually creates very strong trend (RSI 100). // Let's temper the 15m trend to be "Healthy" (RSI ~ 60). // Or just check that it passes "Bullish Momentum" but fails "Overbought". // Wait, generateTrend makes RSI 100 which is Overbought. // Let's manually set the 15m candles to be flat then rising slightly. // Actually, simply: // 1H = RSI 40 (Sell) // 15m = RSI 60 (Buy) // Let's trust that Momentum Rule checks RSI nicely. // If Logic is: Bias BUY -> Need Momentum BUY. // Bias is based on 4H (which is Uptrend in our context). // So we need Momentum to be Bullish. // If we use 1H (Bearish), it will fail. // If we use 15m (Bullish), it will pass. const contextScalp = engine['buildContext'](candles4h_up, candles1h_bearish, candles15m_bullish); // Overwrite RSI to be explicit to avoid "generateTrend" randomness contextScalp.rsi_1h = 40; // Bearish contextScalp.rsi_15m = 60; // Bullish // Also Zone Rule uses 15m. // 1H might be away from EMA, 15m might be close. contextScalp.ema20_1h = 90000; // Price 80000 -> Far away contextScalp.ema20_15m = 80000; // Price 80000 -> Close contextScalp.currentPrice = 80000; const momentumRule = engine['rules'].find((r: any) => r.name === 'MomentumRule'); if (!momentumRule) throw new Error('MomentumRule not found'); const resultScalp = await momentumRule.check(contextScalp); if (resultScalp.passed && resultScalp.signal === 'BUY') { logger.info(`✅ PASSED -> Scalping Logic used 15m RSI (${contextScalp.rsi_15m}) correctly.`); } else { logger.error(`❌ FAILED -> Scalping Logic failed. Reason: ${resultScalp.reason}`); logger.info(`Debug: 1H RSI: ${contextScalp.rsi_1h}, 15m RSI: ${contextScalp.rsi_15m}`); } } runTest().catch(console.error);