feat(trading): 实现做市商吃单/挂单模式互斥机制
## 后端 - trading-service ### MarketMakerService - 新增 MarketMakerMode 类型:'idle' | 'taker' | 'maker' - 新增 getCurrentMode() 和 getRunningStatus() 方法获取当前运行状态 - start() (吃单模式): 启动前自动停止挂单模式 - startMaker() (挂单模式): 启动前自动停止吃单模式 - 两种模式互斥,同一时间只能运行一种 ### MarketMakerController - getConfig 接口返回 runningStatus 运行状态 - 新增 GET /status 接口获取做市商运行状态 ## 前端 - mining-admin-web ### 做市商管理页面 - 新增运行模式状态卡片,显示当前模式(空闲/吃单/挂单) - 吃单模式和挂单模式使用 runningStatus 判断状态 - 添加互斥提示:启动一个模式会自动停止另一个 - 挂单模式添加警告提示:卖单被吃会触发销毁导致价格上涨 ### API 更新 - 新增 RunningStatus 接口类型 - getConfig 返回类型增加 runningStatus - 新增 getRunningStatus API ## 设计说明 - 吃单模式(推荐):做市商只作为买方,不触发额外销毁 - 挂单模式(谨慎使用):做市商挂卖单会触发销毁机制 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b6bd29283
commit
8c78f26e6d
|
|
@ -97,11 +97,14 @@ export class MarketMakerController {
|
|||
@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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +127,24 @@ export class MarketMakerController {
|
|||
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],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export enum AssetType {
|
|||
SHARE = 'SHARE',
|
||||
}
|
||||
|
||||
export type MarketMakerMode = 'idle' | 'taker' | 'maker';
|
||||
|
||||
@Injectable()
|
||||
export class MarketMakerService {
|
||||
private readonly logger = new Logger(MarketMakerService.name);
|
||||
|
|
@ -103,6 +105,33 @@ export class MarketMakerService {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运行模式
|
||||
* idle: 空闲(两个模式都未运行)
|
||||
* taker: 吃单模式运行中
|
||||
* maker: 挂单模式运行中
|
||||
*/
|
||||
getCurrentMode(): MarketMakerMode {
|
||||
if (this.isMakerRunning) return 'maker';
|
||||
if (this.isRunning) return 'taker';
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行状态详情
|
||||
*/
|
||||
getRunningStatus(): {
|
||||
mode: MarketMakerMode;
|
||||
takerRunning: boolean;
|
||||
makerRunning: boolean;
|
||||
} {
|
||||
return {
|
||||
mode: this.getCurrentMode(),
|
||||
takerRunning: this.isRunning,
|
||||
makerRunning: this.isMakerRunning,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化做市商配置(如果不存在)
|
||||
*/
|
||||
|
|
@ -333,7 +362,8 @@ export class MarketMakerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 启动做市商
|
||||
* 启动做市商(吃单模式)
|
||||
* 注意:与挂单模式互斥,启动吃单模式会自动停止挂单模式
|
||||
*/
|
||||
async start(name: string = 'MAIN_MARKET_MAKER'): Promise<void> {
|
||||
const config = await this.getConfig(name);
|
||||
|
|
@ -342,17 +372,23 @@ export class MarketMakerService {
|
|||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('Market maker is already running');
|
||||
this.logger.warn('Market maker taker mode is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
// 互斥:如果挂单模式正在运行,先停止它
|
||||
if (this.isMakerRunning) {
|
||||
this.logger.log('Stopping maker mode before starting taker mode (mutual exclusion)');
|
||||
await this.stopMaker(name);
|
||||
}
|
||||
|
||||
await this.prisma.marketMakerConfig.update({
|
||||
where: { id: config.id },
|
||||
data: { isActive: true },
|
||||
data: { isActive: true, makerEnabled: false },
|
||||
});
|
||||
|
||||
this.isRunning = true;
|
||||
this.logger.log(`Market maker ${name} started`);
|
||||
this.logger.log(`Market maker ${name} taker mode started`);
|
||||
|
||||
// 开始吃单循环
|
||||
this.scheduleNextRun(config);
|
||||
|
|
@ -723,6 +759,7 @@ export class MarketMakerService {
|
|||
|
||||
/**
|
||||
* 启动挂单模式(双边深度)
|
||||
* 注意:与吃单模式互斥,启动挂单模式会自动停止吃单模式
|
||||
*/
|
||||
async startMaker(name: string = 'MAIN_MARKET_MAKER'): Promise<void> {
|
||||
const configRecord = await this.prisma.marketMakerConfig.findUnique({
|
||||
|
|
@ -737,9 +774,15 @@ export class MarketMakerService {
|
|||
return;
|
||||
}
|
||||
|
||||
// 互斥:如果吃单模式正在运行,先停止它
|
||||
if (this.isRunning) {
|
||||
this.logger.log('Stopping taker mode before starting maker mode (mutual exclusion)');
|
||||
await this.stop(name);
|
||||
}
|
||||
|
||||
await this.prisma.marketMakerConfig.update({
|
||||
where: { id: configRecord.id },
|
||||
data: { makerEnabled: true },
|
||||
data: { makerEnabled: true, isActive: false },
|
||||
});
|
||||
|
||||
this.isMakerRunning = true;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export default function MarketMakerPage() {
|
|||
const { data: depthData, isLoading: depthLoading } = useDepth(10);
|
||||
const { data: depthEnabled, isLoading: depthEnabledLoading } = useDepthEnabled();
|
||||
|
||||
const runningStatus = configData?.runningStatus;
|
||||
|
||||
const initializeMutation = useInitializeMarketMaker();
|
||||
const depositCashMutation = useDepositCash();
|
||||
const withdrawCashMutation = useWithdrawCash();
|
||||
|
|
@ -341,6 +343,49 @@ export default function MarketMakerPage() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 运行模式状态 */}
|
||||
<Card className="border-2 border-primary/20">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">当前运行模式</CardTitle>
|
||||
{runningStatus?.mode === 'idle' && (
|
||||
<Badge variant="secondary" className="text-base px-3 py-1">
|
||||
<Pause className="h-4 w-4 mr-1" />
|
||||
空闲
|
||||
</Badge>
|
||||
)}
|
||||
{runningStatus?.mode === 'taker' && (
|
||||
<Badge variant="default" className="bg-green-500 text-base px-3 py-1">
|
||||
<Zap className="h-4 w-4 mr-1" />
|
||||
吃单模式运行中
|
||||
</Badge>
|
||||
)}
|
||||
{runningStatus?.mode === 'maker' && (
|
||||
<Badge variant="default" className="bg-blue-500 text-base px-3 py-1">
|
||||
<BarChart3 className="h-4 w-4 mr-1" />
|
||||
挂单模式运行中
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${runningStatus?.takerRunning ? 'bg-green-500' : 'bg-gray-300'}`} />
|
||||
<span>吃单模式: {runningStatus?.takerRunning ? '运行中' : '未运行'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${runningStatus?.makerRunning ? 'bg-blue-500' : 'bg-gray-300'}`} />
|
||||
<span>挂单模式: {runningStatus?.makerRunning ? '运行中' : '未运行'}</span>
|
||||
</div>
|
||||
<div className="ml-auto text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
|
||||
<AlertCircle className="h-3 w-3 inline mr-1" />
|
||||
两种模式互斥,启动一个会自动停止另一个
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 运行模式控制 */}
|
||||
<Tabs defaultValue="taker">
|
||||
<TabsList>
|
||||
|
|
@ -359,9 +404,11 @@ export default function MarketMakerPage() {
|
|||
<Zap className="h-5 w-5" />
|
||||
吃单模式
|
||||
</CardTitle>
|
||||
<CardDescription>自动买入用户卖单(Taker 策略)</CardDescription>
|
||||
<CardDescription>
|
||||
自动买入用户卖单(Taker 策略)— 做市商作为买方,不触发额外销毁
|
||||
</CardDescription>
|
||||
</div>
|
||||
{config.isActive ? (
|
||||
{runningStatus?.takerRunning ? (
|
||||
<Badge variant="default" className="bg-green-500">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
运行中
|
||||
|
|
@ -396,7 +443,7 @@ export default function MarketMakerPage() {
|
|||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4 border-t">
|
||||
{config.isActive ? (
|
||||
{runningStatus?.takerRunning ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => stopTakerMutation.mutate()}
|
||||
|
|
@ -423,6 +470,13 @@ export default function MarketMakerPage() {
|
|||
{takeOrderMutation.isPending ? '执行中...' : '手动吃单'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{runningStatus?.makerRunning && (
|
||||
<div className="text-sm text-yellow-600 bg-yellow-50 p-3 rounded">
|
||||
<AlertCircle className="h-4 w-4 inline mr-1" />
|
||||
启动吃单模式将自动停止当前运行的挂单模式
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -438,10 +492,12 @@ export default function MarketMakerPage() {
|
|||
<BarChart3 className="h-5 w-5" />
|
||||
挂单模式(双边深度)
|
||||
</CardTitle>
|
||||
<CardDescription>自动挂买卖单形成深度(Maker 策略)</CardDescription>
|
||||
<CardDescription>
|
||||
自动挂买卖单形成深度(Maker 策略)— 做市商卖单被吃会触发销毁
|
||||
</CardDescription>
|
||||
</div>
|
||||
{config.makerEnabled ? (
|
||||
<Badge variant="default" className="bg-green-500">
|
||||
{runningStatus?.makerRunning ? (
|
||||
<Badge variant="default" className="bg-blue-500">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
运行中
|
||||
</Badge>
|
||||
|
|
@ -455,6 +511,13 @@ export default function MarketMakerPage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* 警告提示 */}
|
||||
<div className="text-sm text-orange-600 bg-orange-50 p-3 rounded border border-orange-200">
|
||||
<AlertCircle className="h-4 w-4 inline mr-1" />
|
||||
<strong>注意:</strong>挂单模式下做市商会挂卖单,当卖单被用户买入时会触发销毁机制导致价格上涨。
|
||||
如果只想提供流动性而不影响价格,请使用吃单模式。
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">买单档位</p>
|
||||
|
|
@ -483,7 +546,7 @@ export default function MarketMakerPage() {
|
|||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4 border-t">
|
||||
{config.makerEnabled ? (
|
||||
{runningStatus?.makerRunning ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => stopMakerMutation.mutate()}
|
||||
|
|
@ -494,6 +557,7 @@ export default function MarketMakerPage() {
|
|||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => startMakerMutation.mutate()}
|
||||
disabled={startMakerMutation.isPending}
|
||||
>
|
||||
|
|
@ -504,7 +568,7 @@ export default function MarketMakerPage() {
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => refreshOrdersMutation.mutate()}
|
||||
disabled={refreshOrdersMutation.isPending}
|
||||
disabled={refreshOrdersMutation.isPending || !runningStatus?.makerRunning}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
{refreshOrdersMutation.isPending ? '刷新中...' : '刷新挂单'}
|
||||
|
|
@ -517,6 +581,13 @@ export default function MarketMakerPage() {
|
|||
{cancelAllOrdersMutation.isPending ? '取消中...' : '取消所有挂单'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{runningStatus?.takerRunning && (
|
||||
<div className="text-sm text-yellow-600 bg-yellow-50 p-3 rounded">
|
||||
<AlertCircle className="h-4 w-4 inline mr-1" />
|
||||
启动挂单模式将自动停止当前运行的吃单模式
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ tradingClient.interceptors.response.use(
|
|||
}
|
||||
);
|
||||
|
||||
export type MarketMakerMode = 'idle' | 'taker' | 'maker';
|
||||
|
||||
export interface RunningStatus {
|
||||
mode: MarketMakerMode;
|
||||
takerRunning: boolean;
|
||||
makerRunning: boolean;
|
||||
modeDescription?: string;
|
||||
}
|
||||
|
||||
export interface MarketMakerConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -90,12 +99,22 @@ export interface DepthData {
|
|||
}
|
||||
|
||||
export const marketMakerApi = {
|
||||
// 获取做市商配置
|
||||
getConfig: async (name: string = 'MAIN_MARKET_MAKER'): Promise<{ success: boolean; config: MarketMakerConfig | null }> => {
|
||||
// 获取做市商配置(包含运行状态)
|
||||
getConfig: async (name: string = 'MAIN_MARKET_MAKER'): Promise<{
|
||||
success: boolean;
|
||||
config: MarketMakerConfig | null;
|
||||
runningStatus: RunningStatus;
|
||||
}> => {
|
||||
const response = await tradingClient.get(`/admin/market-maker/${name}/config`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 获取运行状态
|
||||
getRunningStatus: async (): Promise<{ success: boolean } & RunningStatus> => {
|
||||
const response = await tradingClient.get('/admin/market-maker/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 初始化做市商
|
||||
initialize: async (data: {
|
||||
name?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue