import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals' import { UnifiedAccountManager, AccountConfig } from '../../src/accounts/UnifiedAccountManager' import { ExchangeAdapter, Balance, Position } from '../../src/exchanges/ExchangeAdapter' import { EventEmitter } from 'events' // Mock dependencies jest.mock('../../src/accounts/accountManager') jest.mock('../../src/core/hedging/UnifiedHedgingExecutor') jest.mock('../../src/exchanges/AdapterFactory') // Mock ExchangeAdapter class MockAdapter extends EventEmitter implements Partial { async balances(): Promise { return [ { asset: 'USDT', free: '1000', total: '1000', }, ] } async positions(): Promise { return [ { symbol: 'BTCUSDT', qty: '0.1', side: 'LONG', entryPrice: '50000', unrealizedPnl: '100', leverage: 1, }, ] } async symbols(): Promise { return ['BTCUSDT', 'ETHUSDT'] } async depth(): Promise { return { bids: [['50000', '1']], asks: [['50100', '1']], } } async placeOrder(): Promise { return { orderId: '12345', symbol: 'BTCUSDT', side: 'BUY', type: 'MARKET', quantity: '0.1', status: 'FILLED', } } ws(): EventEmitter { return this } } describe('UnifiedAccountManager', () => { let manager: UnifiedAccountManager let mockAccountConfig: AccountConfig beforeEach(() => { // Clear all mocks jest.clearAllMocks() // Create manager instance manager = new UnifiedAccountManager({ maxRetries: 3, timeoutMs: 30000, atomicTimeout: 5000, enableRollback: true, slippageTolerance: 0.005, positionSizeLimit: 1000, }) // Mock account config mockAccountConfig = { exchange: 'binance', accountId: 'test-account', alias: '测试账户', enabled: true, priority: 1, tradingEnabled: true, hedgingEnabled: true, maxPositionUsd: 10000, maxDailyVolumeUsd: 100000, } }) afterEach(() => { manager?.destroy() }) describe('账户注册', () => { it('应该成功注册单个账户', async () => { // Mock AdapterFactory const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory') ;(AdapterFactory.createFromEnv as jest.MockedFunction).mockResolvedValue({ initialized: true, adapter: new MockAdapter(), }) // Mock AccountManager const { AccountManager } = await import('../../src/accounts/accountManager') const mockRegister = jest.fn() ;(AccountManager as jest.Mock).mockImplementation(() => ({ register: mockRegister, ws: () => new EventEmitter(), })) let registeredEvent: any = null manager.on('account_registered', data => { registeredEvent = data }) await manager.registerAccount(mockAccountConfig) expect(mockRegister).toHaveBeenCalledWith({ exchange: 'mock', accountId: 'test-account', adapter: expect.any(MockAdapter), meta: { alias: '测试账户', priority: 1, tradingEnabled: true, hedgingEnabled: true, }, }) expect(registeredEvent).toEqual({ accountKey: 'mock::test-account', config: mockAccountConfig, }) }) it('应该批量注册多个账户', async () => { const configs: AccountConfig[] = [ { ...mockAccountConfig, accountId: 'account-1', alias: '账户1' }, { ...mockAccountConfig, accountId: 'account-2', alias: '账户2' }, ] // Mock AdapterFactory const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory') ;(AdapterFactory.createFromEnv as jest.MockedFunction).mockResolvedValue({ initialized: true, adapter: new MockAdapter(), }) // Mock AccountManager const { AccountManager } = await import('../../src/accounts/accountManager') const mockRegister = jest.fn() ;(AccountManager as jest.Mock).mockImplementation(() => ({ register: mockRegister, ws: () => new EventEmitter(), })) await manager.registerAccountsFromConfig(configs) expect(mockRegister).toHaveBeenCalledTimes(2) }) it('应该处理注册失败', async () => { // Mock AdapterFactory to fail const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory') ;(AdapterFactory.createFromEnv as jest.MockedFunction).mockResolvedValue({ initialized: false, adapter: null, }) let failedEvent: any = null manager.on('account_register_failed', data => { failedEvent = data }) await expect(manager.registerAccount(mockAccountConfig)).rejects.toThrow() expect(failedEvent).toBeTruthy() expect(failedEvent.accountKey).toBe('mock::test-account') }) }) describe('账户管理', () => { beforeEach(async () => { // Setup registered account const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory') ;(AdapterFactory.createFromEnv as jest.MockedFunction).mockResolvedValue({ initialized: true, adapter: new MockAdapter(), }) const { AccountManager } = await import('../../src/accounts/accountManager') ;(AccountManager as jest.Mock).mockImplementation(() => ({ register: jest.fn(), unregister: jest.fn(), getAdapter: jest.fn(() => new MockAdapter()), ws: () => new EventEmitter(), balancesAll: jest.fn(() => Promise.resolve([ { exchange: 'mock', accountId: 'test-account', asset: 'USDT', free: '1000', locked: '0', total: '1000', }, ]), ), positionsAll: jest.fn(() => Promise.resolve([ { exchange: 'mock', accountId: 'test-account', symbol: 'BTCUSDT', qty: '0.1', side: 'LONG', entryPrice: '50000', unrealizedPnl: '100', }, ]), ), placeOrderOn: jest.fn(() => Promise.resolve({ orderId: '12345', symbol: 'BTCUSDT', side: 'BUY', }), ), })) await manager.registerAccount(mockAccountConfig) }) it('应该获取聚合余额', async () => { const balances = await manager.getAggregatedBalances() expect(balances).toHaveProperty('USDT') expect(balances.USDT.total).toBe(1000) expect(balances.USDT.accounts).toHaveLength(1) expect(balances.USDT.accounts[0].accountKey).toBe('mock::test-account') }) it('应该获取聚合仓位', async () => { const positions = await manager.getAggregatedPositions() expect(positions).toHaveProperty('BTCUSDT') expect(positions.BTCUSDT.netSize).toBe(0.1) // LONG side expect(positions.BTCUSDT.accounts).toHaveLength(1) }) it('应该获取账户摘要', async () => { // Wait for account summary to be created await new Promise(resolve => setTimeout(resolve, 100)) const summary = manager.getAccountSummary('mock::test-account') expect(summary).toBeTruthy() expect(summary?.accountKey).toBe('mock::test-account') expect(summary?.exchange).toBe('mock') expect(summary?.alias).toBe('测试账户') }) it('应该卸载账户', async () => { const { AccountManager } = await import('../../src/accounts/accountManager') const accountManagerInstance = (manager as any).accountManager const mockUnregister = jest.spyOn(accountManagerInstance, 'unregister') let unregisteredEvent: any = null manager.on('account_unregistered', data => { unregisteredEvent = data }) await manager.unregisterAccount('mock::test-account') expect(mockUnregister).toHaveBeenCalledWith('mock', 'test-account') expect(unregisteredEvent).toEqual({ accountKey: 'mock::test-account' }) }) }) describe('智能路由', () => { beforeEach(async () => { // Setup registered account const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory') ;(AdapterFactory.createFromEnv as jest.MockedFunction).mockResolvedValue({ initialized: true, adapter: new MockAdapter(), }) const { AccountManager } = await import('../../src/accounts/accountManager') ;(AccountManager as jest.Mock).mockImplementation(() => ({ register: jest.fn(), getAdapter: jest.fn(() => new MockAdapter()), ws: () => new EventEmitter(), placeOrderOn: jest.fn(() => Promise.resolve({ orderId: '12345', symbol: 'BTCUSDT', side: 'BUY', type: 'MARKET', quantity: '0.1', }), ), })) await manager.registerAccount(mockAccountConfig) }) it('应该选择最优账户执行订单', async () => { const result = await manager.smartOrderRouting('BTCUSDT', 'BUY', '0.1', 'MARKET') expect(result.accountKey).toBe('mock::test-account') expect(result.order.symbol).toBe('BTCUSDT') expect(result.order.side).toBe('BUY') }) it('应该在没有可用账户时抛出错误', async () => { // Disable the account await manager.unregisterAccount('mock::test-account') await expect(manager.smartOrderRouting('BTCUSDT', 'BUY', '0.1')).rejects.toThrow('没有可用账户交易 BTCUSDT') }) }) describe('对冲组管理', () => { beforeEach(async () => { // Setup registered account const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory') ;(AdapterFactory.createFromEnv as jest.MockedFunction).mockResolvedValue({ initialized: true, adapter: new MockAdapter(), }) const { AccountManager } = await import('../../src/accounts/accountManager') ;(AccountManager as jest.Mock).mockImplementation(() => ({ register: jest.fn(), getAdapter: jest.fn(() => new MockAdapter()), ws: () => new EventEmitter(), positionsAll: jest.fn(() => Promise.resolve([])), })) await manager.registerAccount(mockAccountConfig) }) it('应该创建对冲组', () => { let createdEvent: any = null manager.on('hedging_group_created', group => { createdEvent = group }) const hedgingGroup = { name: '主对冲组', accounts: ['mock::test-account'], strategy: 'delta_neutral' as const, maxExposureUsd: 10000, enabled: true, } manager.createHedgingGroup(hedgingGroup) expect(createdEvent).toEqual(hedgingGroup) }) it('应该验证对冲组中的账户存在', () => { const hedgingGroup = { name: '测试组', accounts: ['nonexistent::account'], strategy: 'delta_neutral' as const, maxExposureUsd: 10000, enabled: true, } expect(() => { manager.createHedgingGroup(hedgingGroup) }).toThrow('对冲组中的账户不存在: nonexistent::account') }) it('应该验证账户启用对冲功能', () => { // Create account with hedging disabled const disabledHedgingConfig = { ...mockAccountConfig, accountId: 'no-hedge', hedgingEnabled: false, } const hedgingGroup = { name: '测试组', accounts: ['mock::no-hedge'], strategy: 'delta_neutral' as const, maxExposureUsd: 10000, enabled: true, } // Register the account first ;(manager as any).accountConfigs.set('mock::no-hedge', disabledHedgingConfig) expect(() => { manager.createHedgingGroup(hedgingGroup) }).toThrow('账户未启用对冲功能: mock::no-hedge') }) }) describe('系统统计', () => { it('应该返回系统统计信息', () => { const stats = manager.getSystemStats() expect(stats).toHaveProperty('accounts') expect(stats).toHaveProperty('equity') expect(stats).toHaveProperty('volume') expect(stats).toHaveProperty('hedging') expect(stats).toHaveProperty('hedgingGroups') expect(stats.accounts).toHaveProperty('total') expect(stats.accounts).toHaveProperty('online') expect(stats.accounts).toHaveProperty('offline') expect(stats.accounts).toHaveProperty('error') }) }) describe('资源清理', () => { it('应该正确清理资源', () => { const clearIntervalSpy = jest.spyOn(global, 'clearInterval') manager.destroy() expect(clearIntervalSpy).toHaveBeenCalled() expect((manager as any).accountManager).toBeNull() expect((manager as any).hedgingExecutor).toBeNull() }) }) })