learning_ai_invt_trdg/backend/test_pro_strategy.ts

215 lines
8.8 KiB
TypeScript

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<Candle[]> {
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<any> {
return { id: 'mock-order' };
}
async getPosition(symbol: string): Promise<any> {
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);