182 lines
7.9 KiB
TypeScript
182 lines
7.9 KiB
TypeScript
|
|
import { supabaseService } from '../src/services/SupabaseService.js';
|
|
import { AutoTrader } from '../src/services/AutoTrader.js';
|
|
import { TradeExecutor } from '../src/services/TradeExecutor.js';
|
|
import { AlpacaConnector } from '../src/connectors/alpaca.js';
|
|
import { config } from '../src/config/index.js';
|
|
import { SignalDirection, MarketContext, RuleResult, StrategyAnalysisResult } from '../src/strategies/rules/types.js';
|
|
import logger from '../src/utils/logger.js';
|
|
|
|
// Setup environment and global config overrides for testing
|
|
config.ENABLE_TRADING = true;
|
|
// Force Paper Trading for safety
|
|
config.PAPER_TRADING = true;
|
|
|
|
const TEST_SYMBOL = 'BTC/USD'; // Alpaca Paper usually supports this for crypto
|
|
const SLEEP_MS = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
async function runFullE2ETest() {
|
|
logger.info('===================================================');
|
|
logger.info('🚀 STARTING END-TO-END BACKEND SYSTEM TEST');
|
|
logger.info(' Scope: Configs -> Signal -> Order -> Position -> DB -> Exit -> PnL');
|
|
logger.info('===================================================');
|
|
|
|
// 1. Load Profiles (Simulating "Multiple Configurations")
|
|
logger.info('\n📡 1. Loading Configurations from Database...');
|
|
const profiles = await supabaseService.getActiveProfiles();
|
|
|
|
if (profiles.length === 0) {
|
|
logger.error('❌ No active profiles found in DB. Please create a Strategy Cluster in the Dashboard first.');
|
|
process.exit(1);
|
|
}
|
|
logger.info(`✅ Found ${profiles.length} Active Profiles.`);
|
|
|
|
// 2. Iterate Profiles and Execute Test Cycle
|
|
for (const profile of profiles) {
|
|
logger.info(`\n---------------------------------------------------`);
|
|
logger.info(`👤 Testing Profile: [${profile.name}] (ID: ${profile.id})`);
|
|
logger.info(` Allocated Capital: $${profile.allocated_capital}`);
|
|
logger.info(` Risk Per Trade: ${profile.risk_per_trade_percent}%`);
|
|
|
|
const user = profile.users;
|
|
if (!user) {
|
|
logger.warn(' ⚠️ User data missing for this profile. Skipping.');
|
|
continue;
|
|
}
|
|
|
|
// Initialize Services for this specific User/Profile
|
|
const userKey = config.PAPER_TRADING ? user.ALPACA_API_KEY : user.REAL_ALPACA_API_KEY;
|
|
const userSecret = config.PAPER_TRADING ? user.ALPACA_SECRET_KEY : user.REAL_ALPACA_SECRET_KEY;
|
|
|
|
if (!userKey || !userSecret) {
|
|
logger.warn(' ⚠️ Alpaca Credentials missing. Skipping.');
|
|
continue;
|
|
}
|
|
|
|
const exchange = new AlpacaConnector(userKey, userSecret);
|
|
const executor = new TradeExecutor(exchange, undefined, user.user_id, profile.id);
|
|
const trader = new AutoTrader(executor, exchange, profile);
|
|
|
|
// SYNC: Ensure we start clean
|
|
logger.info(' 🔄 Syncing existing positions...');
|
|
await executor.syncPositions([TEST_SYMBOL]);
|
|
|
|
// Close any existing position on Test Symbol to ensure clean test
|
|
const existingPos = executor.getActivePosition(TEST_SYMBOL);
|
|
if (existingPos) {
|
|
logger.info(` 🧹 Closing pre-existing position on ${TEST_SYMBOL}...`);
|
|
await executor.closePosition(TEST_SYMBOL, 'E2E PRE-CLEANUP');
|
|
await SLEEP_MS(5000);
|
|
}
|
|
|
|
// 3. Simulate Market Context
|
|
const currentPrice = 65000;
|
|
const mockContext: MarketContext = {
|
|
symbol: TEST_SYMBOL,
|
|
currentPrice: currentPrice,
|
|
candles1h: [], candles15m: [], candles4h: [],
|
|
rsi_1h: 30, // Oversold
|
|
ema20_1h: 64000,
|
|
change24h: 1.5,
|
|
changeToday: 0.5,
|
|
volatility: 'Low',
|
|
session: 'NY',
|
|
isMajorSession: true,
|
|
latestSignal: SignalDirection.NONE
|
|
};
|
|
|
|
// 4. Simulate BUY Signal
|
|
logger.info(' 🟢 Simulating BUY Signal (Trend + Momentum)...');
|
|
|
|
// Construct Analysis Result that passes "Logic"
|
|
// We ensure a 'generic' passing rule if the profile has specific rules, or rely on global if none.
|
|
const ruleResults: Record<string, RuleResult> = {};
|
|
|
|
// Mock common rules to PASS
|
|
const rulesToMock = ['TrendBiasRule', 'MomentumRule', 'ZoneRule', 'RiskManagementRule'];
|
|
rulesToMock.forEach(rId => {
|
|
ruleResults[rId] = { ruleName: rId, passed: true, signal: SignalDirection.BUY, reason: 'Test Pass', metadata: {} };
|
|
});
|
|
|
|
const buyAnalysis: StrategyAnalysisResult = {
|
|
symbol: TEST_SYMBOL,
|
|
globalSignal: SignalDirection.BUY,
|
|
rules: ruleResults,
|
|
context: mockContext
|
|
};
|
|
|
|
await trader.handleSignal(TEST_SYMBOL, buyAnalysis);
|
|
|
|
// 5. Verify Order & Position Creation
|
|
logger.info(' ⏳ Waiting for Order Fill & DB Sync (10s)...');
|
|
await SLEEP_MS(10000); // Wait for API calls and DB events
|
|
|
|
const activePos = executor.getActivePosition(TEST_SYMBOL);
|
|
if (!activePos) {
|
|
logger.error(` ❌ FAIL: Position not created for ${profile.name}`);
|
|
continue; // Skip to next profile
|
|
}
|
|
|
|
logger.info(` ✅ SUCCESS: Position Created!`);
|
|
logger.info(` Qty: ${activePos.size}`);
|
|
logger.info(` Entry: ${activePos.entryPrice}`);
|
|
logger.info(` Initial U.PnL: ${activePos.unrealizedPnl}`);
|
|
|
|
// Check DB for Order
|
|
const latestOrder = await supabaseService.getLatestOrder(user.user_id, TEST_SYMBOL);
|
|
if (latestOrder && latestOrder.side === 'buy' && latestOrder.status === 'filled') {
|
|
logger.info(` ✅ DB VERIFIED: Buy Order ${latestOrder.order_id} recorded.`);
|
|
} else {
|
|
logger.warn(` ⚠️ DB WARNING: Could not find recent filled buy order.`);
|
|
}
|
|
|
|
// 6. Simulate Profit Update (Price Move Up)
|
|
logger.info(' 📈 Simulating Price Move (+10%)...');
|
|
const newPrice = currentPrice * 1.10;
|
|
mockContext.currentPrice = newPrice;
|
|
|
|
// 7. Simulate SELL/EXIT Signal
|
|
logger.info(' 🔴 Simulating SELL/EXIT Signal (Trend Reversal)...');
|
|
const sellAnalysis: StrategyAnalysisResult = {
|
|
symbol: TEST_SYMBOL,
|
|
globalSignal: SignalDirection.SELL, // Reversal triggers exit
|
|
rules: ruleResults,
|
|
context: mockContext
|
|
};
|
|
|
|
await trader.handleSignal(TEST_SYMBOL, sellAnalysis);
|
|
|
|
// 8. Verify Exit & PnL Realization
|
|
logger.info(' ⏳ Waiting for Close & Profit Realization (10s)...');
|
|
await SLEEP_MS(10000);
|
|
|
|
const closedPos = executor.getActivePosition(TEST_SYMBOL);
|
|
if (!closedPos) {
|
|
logger.info(` ✅ SUCCESS: Position Closed.`);
|
|
} else {
|
|
logger.error(` ❌ FAIL: Position still active!`);
|
|
}
|
|
|
|
// Check DB for Trade History (PnL)
|
|
// We verify the 'logTransaction' was called
|
|
// Since we don't have a direct 'getLatestTransaction', we infer success from logs or check orders
|
|
const sellOrder = await supabaseService.getLatestOrder(user.user_id, TEST_SYMBOL);
|
|
if (sellOrder && sellOrder.side === 'sell' && sellOrder.status === 'filled') {
|
|
logger.info(` ✅ DB VERIFIED: Sell Order ${sellOrder.order_id} recorded.`);
|
|
// In a real scenario, we'd query trade_history too, but logged orders confirms the loop.
|
|
}
|
|
|
|
logger.info(` 🎉 Profile [${profile.name}] Test Cycle Complete.`);
|
|
}
|
|
|
|
logger.info('\n===================================================');
|
|
logger.info('✅ E2E TEST SUITE COMPLETED');
|
|
logger.info('===================================================');
|
|
process.exit(0);
|
|
}
|
|
|
|
runFullE2ETest().catch(err => {
|
|
logger.error('CRITICAL TEST FAILURE:', err);
|
|
process.exit(1);
|
|
});
|