UnifiedAccountManager.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'
  2. import { UnifiedAccountManager, AccountConfig } from '../../src/accounts/UnifiedAccountManager'
  3. import { ExchangeAdapter, Balance, Position } from '../../src/exchanges/ExchangeAdapter'
  4. import { EventEmitter } from 'events'
  5. // Mock dependencies
  6. jest.mock('../../src/accounts/accountManager')
  7. jest.mock('../../src/core/hedging/UnifiedHedgingExecutor')
  8. jest.mock('../../src/exchanges/AdapterFactory')
  9. // Mock ExchangeAdapter
  10. class MockAdapter extends EventEmitter implements Partial<ExchangeAdapter> {
  11. async balances(): Promise<Balance[]> {
  12. return [
  13. {
  14. asset: 'USDT',
  15. free: '1000',
  16. total: '1000',
  17. },
  18. ]
  19. }
  20. async positions(): Promise<Position[]> {
  21. return [
  22. {
  23. symbol: 'BTCUSDT',
  24. qty: '0.1',
  25. side: 'LONG',
  26. entryPrice: '50000',
  27. unrealizedPnl: '100',
  28. leverage: 1,
  29. },
  30. ]
  31. }
  32. async symbols(): Promise<string[]> {
  33. return ['BTCUSDT', 'ETHUSDT']
  34. }
  35. async depth(): Promise<any> {
  36. return {
  37. bids: [['50000', '1']],
  38. asks: [['50100', '1']],
  39. }
  40. }
  41. async placeOrder(): Promise<any> {
  42. return {
  43. orderId: '12345',
  44. symbol: 'BTCUSDT',
  45. side: 'BUY',
  46. type: 'MARKET',
  47. quantity: '0.1',
  48. status: 'FILLED',
  49. }
  50. }
  51. ws(): EventEmitter {
  52. return this
  53. }
  54. }
  55. describe('UnifiedAccountManager', () => {
  56. let manager: UnifiedAccountManager
  57. let mockAccountConfig: AccountConfig
  58. beforeEach(() => {
  59. // Clear all mocks
  60. jest.clearAllMocks()
  61. // Create manager instance
  62. manager = new UnifiedAccountManager({
  63. maxRetries: 3,
  64. timeoutMs: 30000,
  65. atomicTimeout: 5000,
  66. enableRollback: true,
  67. slippageTolerance: 0.005,
  68. positionSizeLimit: 1000,
  69. })
  70. // Mock account config
  71. mockAccountConfig = {
  72. exchange: 'binance',
  73. accountId: 'test-account',
  74. alias: '测试账户',
  75. enabled: true,
  76. priority: 1,
  77. tradingEnabled: true,
  78. hedgingEnabled: true,
  79. maxPositionUsd: 10000,
  80. maxDailyVolumeUsd: 100000,
  81. }
  82. })
  83. afterEach(() => {
  84. manager?.destroy()
  85. })
  86. describe('账户注册', () => {
  87. it('应该成功注册单个账户', async () => {
  88. // Mock AdapterFactory
  89. const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory')
  90. ;(AdapterFactory.createFromEnv as jest.MockedFunction<any>).mockResolvedValue({
  91. initialized: true,
  92. adapter: new MockAdapter(),
  93. })
  94. // Mock AccountManager
  95. const { AccountManager } = await import('../../src/accounts/accountManager')
  96. const mockRegister = jest.fn()
  97. ;(AccountManager as jest.Mock).mockImplementation(() => ({
  98. register: mockRegister,
  99. ws: () => new EventEmitter(),
  100. }))
  101. let registeredEvent: any = null
  102. manager.on('account_registered', data => {
  103. registeredEvent = data
  104. })
  105. await manager.registerAccount(mockAccountConfig)
  106. expect(mockRegister).toHaveBeenCalledWith({
  107. exchange: 'mock',
  108. accountId: 'test-account',
  109. adapter: expect.any(MockAdapter),
  110. meta: {
  111. alias: '测试账户',
  112. priority: 1,
  113. tradingEnabled: true,
  114. hedgingEnabled: true,
  115. },
  116. })
  117. expect(registeredEvent).toEqual({
  118. accountKey: 'mock::test-account',
  119. config: mockAccountConfig,
  120. })
  121. })
  122. it('应该批量注册多个账户', async () => {
  123. const configs: AccountConfig[] = [
  124. { ...mockAccountConfig, accountId: 'account-1', alias: '账户1' },
  125. { ...mockAccountConfig, accountId: 'account-2', alias: '账户2' },
  126. ]
  127. // Mock AdapterFactory
  128. const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory')
  129. ;(AdapterFactory.createFromEnv as jest.MockedFunction<any>).mockResolvedValue({
  130. initialized: true,
  131. adapter: new MockAdapter(),
  132. })
  133. // Mock AccountManager
  134. const { AccountManager } = await import('../../src/accounts/accountManager')
  135. const mockRegister = jest.fn()
  136. ;(AccountManager as jest.Mock).mockImplementation(() => ({
  137. register: mockRegister,
  138. ws: () => new EventEmitter(),
  139. }))
  140. await manager.registerAccountsFromConfig(configs)
  141. expect(mockRegister).toHaveBeenCalledTimes(2)
  142. })
  143. it('应该处理注册失败', async () => {
  144. // Mock AdapterFactory to fail
  145. const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory')
  146. ;(AdapterFactory.createFromEnv as jest.MockedFunction<any>).mockResolvedValue({
  147. initialized: false,
  148. adapter: null,
  149. })
  150. let failedEvent: any = null
  151. manager.on('account_register_failed', data => {
  152. failedEvent = data
  153. })
  154. await expect(manager.registerAccount(mockAccountConfig)).rejects.toThrow()
  155. expect(failedEvent).toBeTruthy()
  156. expect(failedEvent.accountKey).toBe('mock::test-account')
  157. })
  158. })
  159. describe('账户管理', () => {
  160. beforeEach(async () => {
  161. // Setup registered account
  162. const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory')
  163. ;(AdapterFactory.createFromEnv as jest.MockedFunction<any>).mockResolvedValue({
  164. initialized: true,
  165. adapter: new MockAdapter(),
  166. })
  167. const { AccountManager } = await import('../../src/accounts/accountManager')
  168. ;(AccountManager as jest.Mock).mockImplementation(() => ({
  169. register: jest.fn(),
  170. unregister: jest.fn(),
  171. getAdapter: jest.fn(() => new MockAdapter()),
  172. ws: () => new EventEmitter(),
  173. balancesAll: jest.fn(() =>
  174. Promise.resolve([
  175. {
  176. exchange: 'mock',
  177. accountId: 'test-account',
  178. asset: 'USDT',
  179. free: '1000',
  180. locked: '0',
  181. total: '1000',
  182. },
  183. ]),
  184. ),
  185. positionsAll: jest.fn(() =>
  186. Promise.resolve([
  187. {
  188. exchange: 'mock',
  189. accountId: 'test-account',
  190. symbol: 'BTCUSDT',
  191. qty: '0.1',
  192. side: 'LONG',
  193. entryPrice: '50000',
  194. unrealizedPnl: '100',
  195. },
  196. ]),
  197. ),
  198. placeOrderOn: jest.fn(() =>
  199. Promise.resolve({
  200. orderId: '12345',
  201. symbol: 'BTCUSDT',
  202. side: 'BUY',
  203. }),
  204. ),
  205. }))
  206. await manager.registerAccount(mockAccountConfig)
  207. })
  208. it('应该获取聚合余额', async () => {
  209. const balances = await manager.getAggregatedBalances()
  210. expect(balances).toHaveProperty('USDT')
  211. expect(balances.USDT.total).toBe(1000)
  212. expect(balances.USDT.accounts).toHaveLength(1)
  213. expect(balances.USDT.accounts[0].accountKey).toBe('mock::test-account')
  214. })
  215. it('应该获取聚合仓位', async () => {
  216. const positions = await manager.getAggregatedPositions()
  217. expect(positions).toHaveProperty('BTCUSDT')
  218. expect(positions.BTCUSDT.netSize).toBe(0.1) // LONG side
  219. expect(positions.BTCUSDT.accounts).toHaveLength(1)
  220. })
  221. it('应该获取账户摘要', async () => {
  222. // Wait for account summary to be created
  223. await new Promise(resolve => setTimeout(resolve, 100))
  224. const summary = manager.getAccountSummary('mock::test-account')
  225. expect(summary).toBeTruthy()
  226. expect(summary?.accountKey).toBe('mock::test-account')
  227. expect(summary?.exchange).toBe('mock')
  228. expect(summary?.alias).toBe('测试账户')
  229. })
  230. it('应该卸载账户', async () => {
  231. const { AccountManager } = await import('../../src/accounts/accountManager')
  232. const accountManagerInstance = (manager as any).accountManager
  233. const mockUnregister = jest.spyOn(accountManagerInstance, 'unregister')
  234. let unregisteredEvent: any = null
  235. manager.on('account_unregistered', data => {
  236. unregisteredEvent = data
  237. })
  238. await manager.unregisterAccount('mock::test-account')
  239. expect(mockUnregister).toHaveBeenCalledWith('mock', 'test-account')
  240. expect(unregisteredEvent).toEqual({ accountKey: 'mock::test-account' })
  241. })
  242. })
  243. describe('智能路由', () => {
  244. beforeEach(async () => {
  245. // Setup registered account
  246. const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory')
  247. ;(AdapterFactory.createFromEnv as jest.MockedFunction<any>).mockResolvedValue({
  248. initialized: true,
  249. adapter: new MockAdapter(),
  250. })
  251. const { AccountManager } = await import('../../src/accounts/accountManager')
  252. ;(AccountManager as jest.Mock).mockImplementation(() => ({
  253. register: jest.fn(),
  254. getAdapter: jest.fn(() => new MockAdapter()),
  255. ws: () => new EventEmitter(),
  256. placeOrderOn: jest.fn(() =>
  257. Promise.resolve({
  258. orderId: '12345',
  259. symbol: 'BTCUSDT',
  260. side: 'BUY',
  261. type: 'MARKET',
  262. quantity: '0.1',
  263. }),
  264. ),
  265. }))
  266. await manager.registerAccount(mockAccountConfig)
  267. })
  268. it('应该选择最优账户执行订单', async () => {
  269. const result = await manager.smartOrderRouting('BTCUSDT', 'BUY', '0.1', 'MARKET')
  270. expect(result.accountKey).toBe('mock::test-account')
  271. expect(result.order.symbol).toBe('BTCUSDT')
  272. expect(result.order.side).toBe('BUY')
  273. })
  274. it('应该在没有可用账户时抛出错误', async () => {
  275. // Disable the account
  276. await manager.unregisterAccount('mock::test-account')
  277. await expect(manager.smartOrderRouting('BTCUSDT', 'BUY', '0.1')).rejects.toThrow('没有可用账户交易 BTCUSDT')
  278. })
  279. })
  280. describe('对冲组管理', () => {
  281. beforeEach(async () => {
  282. // Setup registered account
  283. const { AdapterFactory } = await import('../../src/exchanges/AdapterFactory')
  284. ;(AdapterFactory.createFromEnv as jest.MockedFunction<any>).mockResolvedValue({
  285. initialized: true,
  286. adapter: new MockAdapter(),
  287. })
  288. const { AccountManager } = await import('../../src/accounts/accountManager')
  289. ;(AccountManager as jest.Mock).mockImplementation(() => ({
  290. register: jest.fn(),
  291. getAdapter: jest.fn(() => new MockAdapter()),
  292. ws: () => new EventEmitter(),
  293. positionsAll: jest.fn(() => Promise.resolve([])),
  294. }))
  295. await manager.registerAccount(mockAccountConfig)
  296. })
  297. it('应该创建对冲组', () => {
  298. let createdEvent: any = null
  299. manager.on('hedging_group_created', group => {
  300. createdEvent = group
  301. })
  302. const hedgingGroup = {
  303. name: '主对冲组',
  304. accounts: ['mock::test-account'],
  305. strategy: 'delta_neutral' as const,
  306. maxExposureUsd: 10000,
  307. enabled: true,
  308. }
  309. manager.createHedgingGroup(hedgingGroup)
  310. expect(createdEvent).toEqual(hedgingGroup)
  311. })
  312. it('应该验证对冲组中的账户存在', () => {
  313. const hedgingGroup = {
  314. name: '测试组',
  315. accounts: ['nonexistent::account'],
  316. strategy: 'delta_neutral' as const,
  317. maxExposureUsd: 10000,
  318. enabled: true,
  319. }
  320. expect(() => {
  321. manager.createHedgingGroup(hedgingGroup)
  322. }).toThrow('对冲组中的账户不存在: nonexistent::account')
  323. })
  324. it('应该验证账户启用对冲功能', () => {
  325. // Create account with hedging disabled
  326. const disabledHedgingConfig = {
  327. ...mockAccountConfig,
  328. accountId: 'no-hedge',
  329. hedgingEnabled: false,
  330. }
  331. const hedgingGroup = {
  332. name: '测试组',
  333. accounts: ['mock::no-hedge'],
  334. strategy: 'delta_neutral' as const,
  335. maxExposureUsd: 10000,
  336. enabled: true,
  337. }
  338. // Register the account first
  339. ;(manager as any).accountConfigs.set('mock::no-hedge', disabledHedgingConfig)
  340. expect(() => {
  341. manager.createHedgingGroup(hedgingGroup)
  342. }).toThrow('账户未启用对冲功能: mock::no-hedge')
  343. })
  344. })
  345. describe('系统统计', () => {
  346. it('应该返回系统统计信息', () => {
  347. const stats = manager.getSystemStats()
  348. expect(stats).toHaveProperty('accounts')
  349. expect(stats).toHaveProperty('equity')
  350. expect(stats).toHaveProperty('volume')
  351. expect(stats).toHaveProperty('hedging')
  352. expect(stats).toHaveProperty('hedgingGroups')
  353. expect(stats.accounts).toHaveProperty('total')
  354. expect(stats.accounts).toHaveProperty('online')
  355. expect(stats.accounts).toHaveProperty('offline')
  356. expect(stats.accounts).toHaveProperty('error')
  357. })
  358. })
  359. describe('资源清理', () => {
  360. it('应该正确清理资源', () => {
  361. const clearIntervalSpy = jest.spyOn(global, 'clearInterval')
  362. manager.destroy()
  363. expect(clearIntervalSpy).toHaveBeenCalled()
  364. expect((manager as any).accountManager).toBeNull()
  365. expect((manager as any).hedgingExecutor).toBeNull()
  366. })
  367. })
  368. })