94 lines
3.5 KiB
TypeScript
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();
|