561 lines
16 KiB
TypeScript
561 lines
16 KiB
TypeScript
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,
|
||
};
|
||
}
|
||
}
|