| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- 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<ExchangeAdapter> {
- async balances(): Promise<Balance[]> {
- return [
- {
- asset: 'USDT',
- free: '1000',
- total: '1000',
- },
- ]
- }
- async positions(): Promise<Position[]> {
- return [
- {
- symbol: 'BTCUSDT',
- qty: '0.1',
- side: 'LONG',
- entryPrice: '50000',
- unrealizedPnl: '100',
- leverage: 1,
- },
- ]
- }
- async symbols(): Promise<string[]> {
- return ['BTCUSDT', 'ETHUSDT']
- }
- async depth(): Promise<any> {
- return {
- bids: [['50000', '1']],
- asks: [['50100', '1']],
- }
- }
- async placeOrder(): Promise<any> {
- 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<any>).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<any>).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<any>).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<any>).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<any>).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<any>).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()
- })
- })
- })
|