import { Controller, Get, Post, Body, Param, Query, HttpCode, HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { IsString, IsOptional, IsNumber } from 'class-validator'; import { MarketMakerService, LedgerType, AssetType } from '../../application/services/market-maker.service'; import { Public } from '../../shared/guards/jwt-auth.guard'; // DTO 定义 class InitializeMarketMakerDto { @IsOptional() @IsString() name?: string; @IsString() accountSequence: string; @IsOptional() @IsString() initialCash?: string; @IsOptional() @IsNumber() maxBuyRatio?: number; @IsOptional() @IsNumber() minIntervalMs?: number; @IsOptional() @IsNumber() maxIntervalMs?: number; } class DepositDto { @IsString() amount: string; @IsOptional() @IsString() memo?: string; } class WithdrawDto { @IsString() amount: string; @IsOptional() @IsString() memo?: string; } class UpdateConfigDto { @IsOptional() @IsNumber() maxBuyRatio?: number; @IsOptional() @IsNumber() minIntervalMs?: number; @IsOptional() @IsNumber() maxIntervalMs?: number; @IsOptional() @IsString() priceStrategy?: string; @IsOptional() @IsNumber() discountRate?: number; } class UpdateMakerConfigDto { @IsOptional() bidEnabled?: boolean; @IsOptional() @IsNumber() bidLevels?: number; @IsOptional() @IsNumber() bidSpread?: number; @IsOptional() @IsNumber() bidLevelSpacing?: number; @IsOptional() @IsString() bidQuantityPerLevel?: string; @IsOptional() askEnabled?: boolean; @IsOptional() @IsNumber() askLevels?: number; @IsOptional() @IsNumber() askSpread?: number; @IsOptional() @IsNumber() askLevelSpacing?: number; @IsOptional() @IsString() askQuantityPerLevel?: string; @IsOptional() @IsNumber() refreshIntervalMs?: number; } @ApiTags('Market Maker') @Controller('admin/market-maker') export class MarketMakerController { constructor(private readonly marketMakerService: MarketMakerService) {} @Post('initialize') @Public() // TODO: 生产环境应添加管理员权限验证 @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '初始化做市商' }) @ApiResponse({ status: 200, description: '做市商初始化成功' }) async initialize(@Body() dto: InitializeMarketMakerDto) { const config = await this.marketMakerService.initializeConfig({ name: dto.name, accountSequence: dto.accountSequence, initialCash: dto.initialCash, maxBuyRatio: dto.maxBuyRatio, minIntervalMs: dto.minIntervalMs, maxIntervalMs: dto.maxIntervalMs, }); return { success: true, message: '做市商初始化成功', config: { id: config.id, name: config.name, accountSequence: config.accountSequence, cashBalance: config.cashBalance.toString(), shareBalance: config.shareBalance.toString(), maxBuyRatio: config.maxBuyRatio.toString(), minIntervalMs: config.minIntervalMs, maxIntervalMs: config.maxIntervalMs, isActive: config.isActive, }, }; } @Get(':name/config') @Public() @ApiOperation({ summary: '获取做市商配置' }) @ApiResponse({ status: 200, description: '返回做市商配置' }) async getConfig(@Param('name') name: string) { const config = await this.marketMakerService.getConfig(name); const runningStatus = this.marketMakerService.getRunningStatus(); if (!config) { return { success: false, message: `做市商 ${name} 不存在`, config: null, runningStatus, }; } return { success: true, config: { id: config.id, name: config.name, accountSequence: config.accountSequence, cashBalance: config.cashBalance.toString(), shareBalance: config.shareBalance.toString(), frozenCash: config.frozenCash.toString(), frozenShares: config.frozenShares.toString(), availableCash: config.cashBalance.minus(config.frozenCash).toString(), availableShares: config.shareBalance.minus(config.frozenShares).toString(), maxBuyRatio: config.maxBuyRatio.toString(), minIntervalMs: config.minIntervalMs, maxIntervalMs: config.maxIntervalMs, priceStrategy: config.priceStrategy, discountRate: config.discountRate.toString(), isActive: config.isActive, }, runningStatus, }; } @Get('status') @Public() @ApiOperation({ summary: '获取做市商运行状态' }) @ApiResponse({ status: 200, description: '返回运行状态' }) getRunningStatus() { const status = this.marketMakerService.getRunningStatus(); return { success: true, ...status, modeDescription: { idle: '空闲(未运行)', taker: '吃单模式(自动买入用户卖单)', maker: '挂单模式(双边深度做市)', }[status.mode], }; } @Post(':name/config') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '更新做市商配置' }) @ApiResponse({ status: 200, description: '配置更新成功' }) async updateConfig(@Param('name') name: string, @Body() dto: UpdateConfigDto) { const config = await this.marketMakerService.updateConfig(name, dto); if (!config) { return { success: false, message: `做市商 ${name} 不存在`, }; } return { success: true, message: '配置更新成功', config: { maxBuyRatio: config.maxBuyRatio.toString(), minIntervalMs: config.minIntervalMs, maxIntervalMs: config.maxIntervalMs, priceStrategy: config.priceStrategy, discountRate: config.discountRate.toString(), }, }; } @Post(':name/deposit') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '充值资金到做市商账户' }) @ApiResponse({ status: 200, description: '充值成功' }) async deposit(@Param('name') name: string, @Body() dto: DepositDto) { await this.marketMakerService.deposit(name, dto.amount, dto.memo); const config = await this.marketMakerService.getConfig(name); return { success: true, message: `充值成功: ${dto.amount}`, newBalance: config?.cashBalance.toString(), }; } @Post(':name/withdraw') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '从做市商账户提现' }) @ApiResponse({ status: 200, description: '提现成功' }) async withdraw(@Param('name') name: string, @Body() dto: WithdrawDto) { await this.marketMakerService.withdraw(name, dto.amount, dto.memo); const config = await this.marketMakerService.getConfig(name); return { success: true, message: `提现成功: ${dto.amount}`, newBalance: config?.cashBalance.toString(), }; } @Post(':name/deposit-shares') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '充值积分股到做市商账户' }) @ApiResponse({ status: 200, description: '积分股充值成功' }) async depositShares(@Param('name') name: string, @Body() dto: DepositDto) { await this.marketMakerService.depositShares(name, dto.amount, dto.memo); const config = await this.marketMakerService.getConfig(name); return { success: true, message: `积分股充值成功: ${dto.amount}`, newShareBalance: config?.shareBalance.toString(), }; } @Post(':name/withdraw-shares') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '从做市商账户提取积分股' }) @ApiResponse({ status: 200, description: '积分股提取成功' }) async withdrawShares(@Param('name') name: string, @Body() dto: WithdrawDto) { await this.marketMakerService.withdrawShares(name, dto.amount, dto.memo); const config = await this.marketMakerService.getConfig(name); return { success: true, message: `积分股提取成功: ${dto.amount}`, newShareBalance: config?.shareBalance.toString(), }; } @Post(':name/start') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '启动做市商' }) @ApiResponse({ status: 200, description: '做市商已启动' }) async start(@Param('name') name: string) { await this.marketMakerService.start(name); return { success: true, message: `做市商 ${name} 已启动`, }; } @Post(':name/stop') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '停止做市商' }) @ApiResponse({ status: 200, description: '做市商已停止' }) async stop(@Param('name') name: string) { await this.marketMakerService.stop(name); return { success: true, message: `做市商 ${name} 已停止`, }; } @Post(':name/take-order') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '手动执行一次吃单' }) @ApiResponse({ status: 200, description: '吃单执行结果' }) async takeOrder(@Param('name') name: string) { const result = await this.marketMakerService.executeTakeOrder(name); return result; } @Get(':name/stats') @Public() @ApiOperation({ summary: '获取做市商统计信息' }) @ApiResponse({ status: 200, description: '返回做市商统计' }) async getStats(@Param('name') name: string) { const stats = await this.marketMakerService.getStats(name); if (!stats.config) { return { success: false, message: `做市商 ${name} 不存在`, }; } return { success: true, config: { name: stats.config.name, cashBalance: stats.config.cashBalance.toString(), shareBalance: stats.config.shareBalance.toString(), isActive: stats.config.isActive, }, recentTrades: stats.recentTrades.map((t) => ({ type: t.type, assetType: t.assetType, amount: t.amount.toString(), price: t.price?.toString(), quantity: t.quantity?.toString(), counterpartySeq: t.counterpartySeq, orderNo: t.orderNo, memo: t.memo, createdAt: t.createdAt, })), dailyStats: stats.dailyStats.map((s) => ({ date: s.date, buyCount: s.buyCount, buyQuantity: s.buyQuantity.toString(), buyAmount: s.buyAmount.toString(), avgBuyPrice: s.avgBuyPrice.toString(), sellCount: s.sellCount, sellQuantity: s.sellQuantity.toString(), sellAmount: s.sellAmount.toString(), realizedPnl: s.realizedPnl.toString(), })), }; } @Get(':name/ledgers') @Public() @ApiOperation({ summary: '获取做市商分类账流水' }) @ApiQuery({ name: 'type', required: false, enum: LedgerType }) @ApiQuery({ name: 'assetType', required: false, enum: AssetType }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) @ApiResponse({ status: 200, description: '返回分类账流水' }) async getLedgers( @Param('name') name: string, @Query('type') type?: LedgerType, @Query('assetType') assetType?: AssetType, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { const result = await this.marketMakerService.getLedgers(name, { type, assetType, page: page ? parseInt(page, 10) : undefined, pageSize: pageSize ? parseInt(pageSize, 10) : undefined, }); return { success: true, data: result.data.map((l) => ({ id: l.id, type: l.type, assetType: l.assetType, amount: l.amount.toString(), balanceBefore: l.balanceBefore.toString(), balanceAfter: l.balanceAfter.toString(), tradeNo: l.tradeNo, orderNo: l.orderNo, counterpartySeq: l.counterpartySeq, price: l.price?.toString(), quantity: l.quantity?.toString(), memo: l.memo, createdAt: l.createdAt, })), total: result.total, }; } // ============ 双边挂单(深度做市)相关端点 ============ @Post(':name/start-maker') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '启动挂单模式(双边深度)' }) @ApiResponse({ status: 200, description: '挂单模式已启动' }) async startMaker(@Param('name') name: string) { await this.marketMakerService.startMaker(name); return { success: true, message: `做市商 ${name} 挂单模式已启动`, }; } @Post(':name/stop-maker') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '停止挂单模式' }) @ApiResponse({ status: 200, description: '挂单模式已停止' }) async stopMaker(@Param('name') name: string) { await this.marketMakerService.stopMaker(name); return { success: true, message: `做市商 ${name} 挂单模式已停止`, }; } @Post(':name/refresh-orders') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '手动刷新双边挂单' }) @ApiResponse({ status: 200, description: '挂单刷新结果' }) async refreshOrders(@Param('name') name: string) { const result = await this.marketMakerService.refreshMakerOrders(name); return result; } @Post(':name/cancel-all-orders') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '取消所有做市商挂单' }) @ApiResponse({ status: 200, description: '取消结果' }) async cancelAllOrders(@Param('name') name: string) { const cancelledCount = await this.marketMakerService.cancelAllMakerOrders(name); return { success: true, message: `已取消 ${cancelledCount} 个挂单`, cancelledCount, }; } @Post(':name/maker-config') @Public() @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '更新挂单配置' }) @ApiResponse({ status: 200, description: '挂单配置更新成功' }) async updateMakerConfig(@Param('name') name: string, @Body() dto: UpdateMakerConfigDto) { await this.marketMakerService.updateMakerConfig(name, dto); return { success: true, message: '挂单配置更新成功', }; } @Get(':name/maker-orders') @Public() @ApiOperation({ summary: '获取做市商挂单列表' }) @ApiQuery({ name: 'side', required: false, enum: ['BID', 'ASK'] }) @ApiQuery({ name: 'status', required: false, enum: ['ACTIVE', 'FILLED', 'CANCELLED'] }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) @ApiResponse({ status: 200, description: '返回挂单列表' }) async getMakerOrders( @Param('name') name: string, @Query('side') side?: 'BID' | 'ASK', @Query('status') status?: 'ACTIVE' | 'FILLED' | 'CANCELLED', @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { const result = await this.marketMakerService.getMakerOrders(name, { side, status, page: page ? parseInt(page, 10) : undefined, pageSize: pageSize ? parseInt(pageSize, 10) : undefined, }); return { success: true, data: result.data.map((o) => ({ id: o.id, orderNo: o.orderNo, side: o.side, level: o.level, price: o.price.toString(), quantity: o.quantity.toString(), remainingQty: o.remainingQty.toString(), status: o.status, createdAt: o.createdAt, })), total: result.total, }; } @Get('depth') @Public() @ApiOperation({ summary: '获取订单簿深度' }) @ApiQuery({ name: 'levels', required: false, type: Number, description: '深度档位数(默认10)' }) @ApiResponse({ status: 200, description: '返回深度数据' }) async getDepth(@Query('levels') levels?: string) { const depth = await this.marketMakerService.getDepth(levels ? parseInt(levels, 10) : 10); return { success: true, ...depth, }; } }