215 lines
8.8 KiB
TypeScript
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);
|