103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { ManualTrader } from '../src/services/ManualTrader.js';
|
|
import { SignalDirection } from '../src/strategies/rules/types.js';
|
|
import { supabaseService } from '../src/services/SupabaseService.js';
|
|
|
|
class MockExecutor {
|
|
public positions = new Map<string, any>();
|
|
public calls: Array<{ symbol: string; side: SignalDirection; qty: number; type: 'market' | 'limit'; price?: number }> = [];
|
|
|
|
getAllPositions() {
|
|
return this.positions;
|
|
}
|
|
|
|
getProfileId() {
|
|
return 'profile-cap-1';
|
|
}
|
|
|
|
getUserId() {
|
|
return 'user-cap-1';
|
|
}
|
|
|
|
getActivePosition(_symbol: string) {
|
|
return null;
|
|
}
|
|
|
|
async closePosition(_symbol: string, _reason: string) {
|
|
return { success: true };
|
|
}
|
|
|
|
async openPosition(
|
|
symbol: string,
|
|
side: SignalDirection,
|
|
qty: number,
|
|
type: 'market' | 'limit',
|
|
price?: number
|
|
) {
|
|
this.calls.push({ symbol, side, qty, type, price });
|
|
return { success: true, orderId: `mock-${Date.now()}` };
|
|
}
|
|
}
|
|
|
|
async function testScaleToRemainingCapital() {
|
|
const executor = new MockExecutor();
|
|
executor.positions.set('BTC/USD', {
|
|
entryPrice: 100,
|
|
size: 9 // committed = 900
|
|
});
|
|
|
|
const trader = new ManualTrader(executor as any);
|
|
const result = await trader.executeRequest('ETH/USD', 'buy', 2, 'market', undefined, 100, 'user-cap-1');
|
|
|
|
assert.equal(result.success, true, 'Manual trade should succeed when some capital remains.');
|
|
assert.equal(Number(result.adjustedQty), 1, 'Quantity should be reduced to remaining capital capacity.');
|
|
assert.equal(executor.calls.length, 1, 'openPosition should be called once.');
|
|
assert.equal(Number(executor.calls[0].qty), 1, 'Executor should receive scaled quantity.');
|
|
}
|
|
|
|
async function testWaitForCapitalRelease() {
|
|
const executor = new MockExecutor();
|
|
executor.positions.set('BTC/USD', {
|
|
entryPrice: 100,
|
|
size: 10 // committed = 1000, no immediate room
|
|
});
|
|
|
|
const trader = new ManualTrader(executor as any);
|
|
|
|
setTimeout(() => {
|
|
executor.positions.set('BTC/USD', {
|
|
entryPrice: 100,
|
|
size: 9 // remaining = 100 after release
|
|
});
|
|
}, 1200);
|
|
|
|
const start = Date.now();
|
|
const result = await trader.executeRequest('SOL/USD', 'buy', 1, 'market', undefined, 100, 'user-cap-1');
|
|
const elapsed = Date.now() - start;
|
|
|
|
assert.equal(result.success, true, 'Manual trade should proceed once capital is released.');
|
|
assert(elapsed >= 1000, 'Manual trader should wait for capital release window.');
|
|
assert.equal(executor.calls.length, 1, 'openPosition should execute after wait.');
|
|
assert.equal(Number(executor.calls[0].qty), 1, 'Released capital should allow requested quantity.');
|
|
}
|
|
|
|
async function run() {
|
|
const originalGetProfileCapital = (supabaseService as any).getProfileCapital;
|
|
(supabaseService as any).getProfileCapital = async () => ({
|
|
allocatedCapital: 1000,
|
|
isActive: true,
|
|
userId: 'user-cap-1'
|
|
});
|
|
|
|
try {
|
|
await testScaleToRemainingCapital();
|
|
await testWaitForCapitalRelease();
|
|
} finally {
|
|
(supabaseService as any).getProfileCapital = originalGetProfileCapital;
|
|
}
|
|
|
|
console.log('[manual-capital-guard] OK: scale-to-remaining and wait-for-release paths validated');
|
|
}
|
|
|
|
await run();
|