rwadurian/backend/services/trading-service/src/api/controllers/market-maker.controller.ts

561 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}
}