learning_ai_invt_trdg/backend/testStrictCapitalGuard.ts

231 lines
9.3 KiB
TypeScript

import assert from 'node:assert/strict';
import { config } from '../src/config/index.js';
import type { Candle, IExchangeConnector } from '../src/connectors/types.js';
import { TradeExecutor } from '../src/services/TradeExecutor.js';
import { capitalLedger } from '../src/services/CapitalLedger.js';
import { distributedLockService } from '../src/services/distributedLockService.js';
import { supabaseService } from '../src/services/SupabaseService.js';
import { SignalDirection } from '../src/strategies/rules/types.js';
class MockExchange implements IExchangeConnector {
public placedQty = 0;
public placedPrice = 0;
private orderSeq = 0;
getCapabilities() {
return {
fetchOpenOrders: true,
fetchClosedOrders: true,
shorting: true
};
}
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> {
this.orderSeq += 1;
this.placedQty = Number(qty);
this.placedPrice = Number(price || 100);
return {
id: `strict-cap-${this.orderSeq}`,
symbol,
side,
qty,
status: 'filled',
filled_avg_price: this.placedPrice,
filled_qty: String(qty)
};
}
async getPosition(_symbol: string): Promise<any> {
return null;
}
async getOrder(orderId: string): Promise<any> {
return {
id: orderId,
status: 'filled',
filled_avg_price: 100,
filled_qty: String(this.placedQty || 1)
};
}
}
const saveConfig = () => ({
ENTRY_CAPITAL_BUFFER_PCT: config.ENTRY_CAPITAL_BUFFER_PCT,
ENABLE_STRICT_CAPITAL_GUARD: config.ENABLE_STRICT_CAPITAL_GUARD,
STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT: config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT,
STRICT_CAPITAL_FEE_BUFFER_PCT: config.STRICT_CAPITAL_FEE_BUFFER_PCT,
STRICT_CAPITAL_MIN_RESERVE_USD: config.STRICT_CAPITAL_MIN_RESERVE_USD
});
type Stubbed = {
hasActiveOrderForTradeId: any;
hasFinalizedTradeHistory: any;
getVirtualOpenPosition: any;
getPendingOrdersForProfile: any;
updateOrderStatus: any;
logOrder: any;
logTransaction: any;
reserveForOrder: any;
releaseOrderReservation: any;
adjustPositionReservation: any;
recordRealizedPnl: any;
getAvailableCapital: any;
tryAcquireRowLock: any;
releaseRowLock: any;
isEntryInProgress: any;
};
const stubDependencies = (availableCapital: number, reservationLog: number[]): Stubbed => {
const originals: Stubbed = {
hasActiveOrderForTradeId: (supabaseService as any).hasActiveOrderForTradeId,
hasFinalizedTradeHistory: (supabaseService as any).hasFinalizedTradeHistory,
getVirtualOpenPosition: (supabaseService as any).getVirtualOpenPosition,
getPendingOrdersForProfile: (supabaseService as any).getPendingOrdersForProfile,
updateOrderStatus: (supabaseService as any).updateOrderStatus,
logOrder: (supabaseService as any).logOrder,
logTransaction: (supabaseService as any).logTransaction,
reserveForOrder: (capitalLedger as any).reserveForOrder,
releaseOrderReservation: (capitalLedger as any).releaseOrderReservation,
adjustPositionReservation: (capitalLedger as any).adjustPositionReservation,
recordRealizedPnl: (capitalLedger as any).recordRealizedPnl,
getAvailableCapital: (capitalLedger as any).getAvailableCapital,
tryAcquireRowLock: (distributedLockService as any).tryAcquireRowLock,
releaseRowLock: (distributedLockService as any).releaseRowLock,
isEntryInProgress: (distributedLockService as any).isEntryInProgress
};
(supabaseService as any).hasActiveOrderForTradeId = async () => false;
(supabaseService as any).hasFinalizedTradeHistory = async () => false;
(supabaseService as any).getVirtualOpenPosition = async () => null;
(supabaseService as any).getPendingOrdersForProfile = async () => [];
(supabaseService as any).updateOrderStatus = async () => { };
(supabaseService as any).logOrder = async () => { };
(supabaseService as any).logTransaction = async () => { };
(capitalLedger as any).getAvailableCapital = async () => availableCapital;
(capitalLedger as any).reserveForOrder = async (_profileId: string, amount: number) => {
reservationLog.push(Number(amount));
return true;
};
(capitalLedger as any).releaseOrderReservation = async () => { };
(capitalLedger as any).adjustPositionReservation = async () => { };
(capitalLedger as any).recordRealizedPnl = async () => { };
(distributedLockService as any).tryAcquireRowLock = async () => true;
(distributedLockService as any).releaseRowLock = async () => true;
(distributedLockService as any).isEntryInProgress = async () => false;
return originals;
};
const restoreDependencies = (originals: Stubbed) => {
(supabaseService as any).hasActiveOrderForTradeId = originals.hasActiveOrderForTradeId;
(supabaseService as any).hasFinalizedTradeHistory = originals.hasFinalizedTradeHistory;
(supabaseService as any).getVirtualOpenPosition = originals.getVirtualOpenPosition;
(supabaseService as any).getPendingOrdersForProfile = originals.getPendingOrdersForProfile;
(supabaseService as any).updateOrderStatus = originals.updateOrderStatus;
(supabaseService as any).logOrder = originals.logOrder;
(supabaseService as any).logTransaction = originals.logTransaction;
(capitalLedger as any).reserveForOrder = originals.reserveForOrder;
(capitalLedger as any).releaseOrderReservation = originals.releaseOrderReservation;
(capitalLedger as any).adjustPositionReservation = originals.adjustPositionReservation;
(capitalLedger as any).recordRealizedPnl = originals.recordRealizedPnl;
(capitalLedger as any).getAvailableCapital = originals.getAvailableCapital;
(distributedLockService as any).tryAcquireRowLock = originals.tryAcquireRowLock;
(distributedLockService as any).releaseRowLock = originals.releaseRowLock;
(distributedLockService as any).isEntryInProgress = originals.isEntryInProgress;
};
const apiServerStub = {
updatePositions: () => { },
updateOrders: () => { },
addHistory: () => { },
recordOrderFailure: () => { },
getState: () => ({ accountSnapshot: { buying_power: 10_000 } }),
updateAccountSnapshot: () => { },
updateAccountSnapshotCache: () => { }
};
async function runCase(strictEnabled: boolean): Promise<{ placedQty: number; reservedAmount: number }> {
const originalConfig = saveConfig();
const reservations: number[] = [];
const originals = stubDependencies(100, reservations);
try {
config.ENTRY_CAPITAL_BUFFER_PCT = 0;
config.ENABLE_STRICT_CAPITAL_GUARD = strictEnabled;
config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT = 1;
config.STRICT_CAPITAL_FEE_BUFFER_PCT = 0.15;
config.STRICT_CAPITAL_MIN_RESERVE_USD = 0;
const exchange = new MockExchange();
const executor = new TradeExecutor(
exchange,
apiServerStub as any,
'global',
strictEnabled ? 'profile-strict-on' : 'profile-strict-off'
);
const result = await executor.openPosition('BTC/USD', SignalDirection.BUY, 1, 'limit', 100);
executor.dispose();
assert.equal(result.success, true, `Expected entry to succeed (strict=${strictEnabled})`);
assert(exchange.placedQty > 0, 'Expected exchange order to be placed');
assert(reservations.length > 0, 'Expected capital reservation to be recorded');
return {
placedQty: Number(exchange.placedQty),
reservedAmount: Number(reservations[0])
};
} finally {
config.ENTRY_CAPITAL_BUFFER_PCT = originalConfig.ENTRY_CAPITAL_BUFFER_PCT;
config.ENABLE_STRICT_CAPITAL_GUARD = originalConfig.ENABLE_STRICT_CAPITAL_GUARD;
config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT = originalConfig.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT;
config.STRICT_CAPITAL_FEE_BUFFER_PCT = originalConfig.STRICT_CAPITAL_FEE_BUFFER_PCT;
config.STRICT_CAPITAL_MIN_RESERVE_USD = originalConfig.STRICT_CAPITAL_MIN_RESERVE_USD;
restoreDependencies(originals);
}
}
async function run() {
const relaxed = await runCase(false);
const strict = await runCase(true);
assert.strictEqual(relaxed.placedQty, 1, 'Without strict guard, qty should use full available notional.');
assert(
strict.placedQty < relaxed.placedQty,
`Strict guard must reduce qty. relaxed=${relaxed.placedQty} strict=${strict.placedQty}`
);
assert(
strict.reservedAmount <= 100 + 1e-6,
`Strict reservation should remain within available capital envelope. strict=${strict.reservedAmount}`
);
assert(
strict.reservedAmount > (strict.placedQty * 100),
`Strict reservation should include slippage/fee buffer. qty=${strict.placedQty} reserved=${strict.reservedAmount}`
);
console.log('[strict-capital-guard] OK: strict guard reduces qty and raises reservation envelope');
}
await run();