learning_ai_invt_trdg/backend/testFailureInjection.ts

94 lines
3.5 KiB
TypeScript

import assert from 'node:assert/strict';
import type { Candle, IExchangeConnector } from '../src/connectors/types.js';
import { TradeExecutor } from '../src/services/TradeExecutor.js';
import { SignalDirection } from '../src/strategies/rules/types.js';
import { OrderStatusSyncService } from '../src/services/OrderStatusSyncService.js';
import { supabaseService } from '../src/services/SupabaseService.js';
class FlakyExchange implements IExchangeConnector {
private failPlaceOrder = false;
private failGetOrder = false;
setPlaceOrderFailure(flag: boolean) {
this.failPlaceOrder = flag;
}
setGetOrderFailure(flag: boolean) {
this.failGetOrder = flag;
}
async fetchOHLCV(_symbol: string, _timeframe: string, _limit?: number): Promise<Candle[]> {
return [{
timestamp: Date.now(),
open: 100,
high: 100,
low: 100,
close: 100,
volume: 1
}];
}
async placeOrder(_symbol: string, _side: 'buy' | 'sell', qty: number, _type: 'market' | 'limit', price?: number): Promise<any> {
if (this.failPlaceOrder) {
throw new Error('simulated-exchange-flap: placeOrder');
}
return {
id: `mock-${Date.now()}`,
status: 'filled',
filled_avg_price: price || 100,
filled_qty: `${qty}`
};
}
async getPosition(_symbol: string): Promise<any> {
return null;
}
async getOrder(_orderId: string): Promise<any> {
if (this.failGetOrder) {
throw new Error('simulated-exchange-flap: getOrder');
}
return { status: 'filled', filled_avg_price: 100, filled_qty: '1' };
}
}
async function run() {
const exchange = new FlakyExchange();
// Failure injection 1: entry should fail safely on exchange placeOrder flap.
const executor = new TradeExecutor(exchange, undefined, 'global', 'failure-test');
exchange.setPlaceOrderFailure(true);
const result = await executor.openPosition('BTC/USD', SignalDirection.BUY, 1, 'market');
assert.equal(result.success, false, 'openPosition must fail safely when exchange placeOrder throws.');
assert.equal(executor.getActivePosition('BTC/USD'), null, 'No local position should be created on failed order placement.');
// Failure injection 2: stale-order sync should absorb getOrder flap without crashing or mutating status.
const originalGetStaleOrders = (supabaseService as any).getStaleOrders;
const originalUpdateOrderStatus = (supabaseService as any).updateOrderStatus;
const statusWrites: string[] = [];
(supabaseService as any).getStaleOrders = async () => [{
order_id: 'ord-flaky',
symbol: 'BTC/USD',
status: 'pending_new',
created_at: new Date(Date.now() - 10 * 60 * 1000).toISOString()
}];
(supabaseService as any).updateOrderStatus = async (_orderId: string, status: string) => {
statusWrites.push(status);
};
exchange.setGetOrderFailure(true);
try {
const sync = new OrderStatusSyncService(exchange, 5_000);
await sync.triggerSync();
assert.equal(statusWrites.length, 0, 'Order status should not be mutated when exchange getOrder fails transiently.');
} finally {
(supabaseService as any).getStaleOrders = originalGetStaleOrders;
(supabaseService as any).updateOrderStatus = originalUpdateOrderStatus;
}
console.log('[failure-injection] OK: exchange/API flap paths handled safely');
}
await run();