feat(trading): 实现完整的CEX做市商双边深度系统

## 后端 - trading-service

### 数据库模型扩展 (Prisma Schema)
- TradingConfig: 新增 depthEnabled 字段控制深度显示开关
- MarketMakerConfig: 新增双边挂单配置
  - makerEnabled: 做市商挂单模式开关
  - bidEnabled/askEnabled: 买/卖方向独立开关
  - bidLevels/askLevels: 买/卖档位数量
  - bidSpread/askSpread: 买/卖价差比例
  - bidLevelSpacing/askLevelSpacing: 档位间距
  - bidQuantityPerLevel/askQuantityPerLevel: 每档数量
  - refreshIntervalMs: 刷新间隔
- MarketMakerOrder: 新增做市商订单追踪模型
- MarketMakerLedger: 新增做市商账户流水模型

### 做市商服务 (MarketMakerService)
- depositShares/withdrawShares: 积分股充值/提现
- startMaker/stopMaker: 做市商挂单模式启停
- refreshMakerOrders: 核心双边挂单逻辑
  - 根据当前价格计算买卖各档位价格和数量
  - 自动撤销旧订单并创建新订单
  - 记录做市商订单关联
- cancelAllMakerOrders: 撤销所有做市商订单
- getDepth: 获取订单簿深度数据
- updateMakerConfig/getMakerOrders: 配置和订单查询

### API 端点
- MarketMakerController:
  - POST /deposit-shares: 积分股充值
  - POST /withdraw-shares: 积分股提现
  - POST /start-maker: 启动挂单模式
  - POST /stop-maker: 停止挂单模式
  - POST /refresh-orders: 手动刷新订单
  - POST /cancel-all-orders: 撤销所有订单
  - PUT /maker-config: 更新挂单配置
  - GET /maker-orders: 查询做市商订单
  - GET /depth: 获取深度数据
- AdminController:
  - GET/POST /trading/depth-enabled: 深度显示开关
- PriceController:
  - GET /depth: 公开深度接口 (受 depthEnabled 控制)

### 领域层扩展
- TradingAccountAggregate: 新增 depositShares/withdrawShares 方法
- OrderAggregate: 支持 source 字段标识订单来源

## 前端 - mining-admin-web

### 做市商管理页面 (/market-maker)
- 账户余额展示: 积分值和积分股余额
- 资金管理: 积分值/积分股的充值和提现对话框
- 吃单模式: 启动/停止/手动吃单控制
- 挂单模式: 启动/停止/刷新订单/撤销所有
- 深度开关: 控制公开 API 是否返回深度数据
- 深度展示: 实时显示买卖盘深度数据表格

### 前端架构
- market-maker.api.ts: 完整的 API 客户端
- use-market-maker.ts: React Query hooks 封装
- sidebar.tsx: 新增"做市商管理"导航菜单

## 数据库迁移
- 0003_add_market_maker_depth: 双边深度相关字段
- 0005_add_market_maker_and_order_source: 订单来源追踪

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-17 21:11:23 -08:00
parent 416495a398
commit 3b6bd29283
21 changed files with 3434 additions and 8 deletions

View File

@ -782,7 +782,9 @@
"Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/mining_db?schema=public\" npx prisma migrate diff:*)",
"Bash(git status:*)",
"Bash(xargs cat:*)",
"Bash(ssh -o ProxyJump=ceshi@103.39.231.231 ceshi@192.168.1.111 \"docker ps | grep mining\")"
"Bash(ssh -o ProxyJump=ceshi@103.39.231.231 ceshi@192.168.1.111 \"docker ps | grep mining\")",
"Bash(dir /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\trading-service\\\\src\\\\application\\\\services\")",
"Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/trading_db?schema=public\" npx prisma migrate dev:*)"
],
"deny": [],
"ask": []

View File

@ -0,0 +1,75 @@
-- ============================================================================
-- trading-service 添加做市商双边深度功能
-- 包含:深度开关、做市商双边挂单配置、做市商挂单记录表
-- ============================================================================
-- ==================== TradingConfig 添加深度开关 ====================
-- 添加深度显示开关
ALTER TABLE "trading_configs" ADD COLUMN IF NOT EXISTS "depth_enabled" BOOLEAN NOT NULL DEFAULT false;
-- ==================== MarketMakerConfig 添加双边挂单配置 ====================
-- 挂单模式开关
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "maker_enabled" BOOLEAN NOT NULL DEFAULT false;
-- 买单挂单配置
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "bid_enabled" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "bid_levels" INTEGER NOT NULL DEFAULT 5;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "bid_spread" DECIMAL(10, 4) NOT NULL DEFAULT 0.01;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "bid_level_spacing" DECIMAL(10, 4) NOT NULL DEFAULT 0.005;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "bid_quantity_per_level" DECIMAL(30, 8) NOT NULL DEFAULT 1000;
-- 卖单挂单配置
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "ask_enabled" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "ask_levels" INTEGER NOT NULL DEFAULT 5;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "ask_spread" DECIMAL(10, 4) NOT NULL DEFAULT 0.01;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "ask_level_spacing" DECIMAL(10, 4) NOT NULL DEFAULT 0.005;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "ask_quantity_per_level" DECIMAL(30, 8) NOT NULL DEFAULT 1000;
-- 挂单刷新配置
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "refresh_interval_ms" INTEGER NOT NULL DEFAULT 60000;
ALTER TABLE "market_maker_configs" ADD COLUMN IF NOT EXISTS "last_refresh_at" TIMESTAMP(3);
-- ==================== 做市商挂单记录表 ====================
-- CreateTable
CREATE TABLE IF NOT EXISTS "market_maker_orders" (
"id" TEXT NOT NULL,
"market_maker_id" TEXT NOT NULL,
"order_id" TEXT NOT NULL,
"order_no" TEXT NOT NULL,
"side" TEXT NOT NULL,
"level" INTEGER NOT NULL,
"price" DECIMAL(30, 18) NOT NULL,
"quantity" DECIMAL(30, 8) NOT NULL,
"remaining_qty" DECIMAL(30, 8) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "market_maker_orders_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "market_maker_orders_order_id_key" ON "market_maker_orders"("order_id");
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "market_maker_orders_order_no_key" ON "market_maker_orders"("order_no");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "market_maker_orders_market_maker_id_side_status_idx" ON "market_maker_orders"("market_maker_id", "side", "status");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "market_maker_orders_status_idx" ON "market_maker_orders"("status");
-- AddForeignKey (only if not exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'market_maker_orders_market_maker_id_fkey'
) THEN
ALTER TABLE "market_maker_orders" ADD CONSTRAINT "market_maker_orders_market_maker_id_fkey"
FOREIGN KEY ("market_maker_id") REFERENCES "market_maker_configs"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;

View File

@ -0,0 +1,98 @@
-- 订单来源字段
ALTER TABLE "orders" ADD COLUMN "source" TEXT NOT NULL DEFAULT 'USER';
ALTER TABLE "orders" ADD COLUMN "source_label" TEXT;
CREATE INDEX "orders_source_idx" ON "orders"("source");
-- 成交记录来源字段
ALTER TABLE "trades" ADD COLUMN "buyer_source" TEXT NOT NULL DEFAULT 'USER';
ALTER TABLE "trades" ADD COLUMN "seller_source" TEXT NOT NULL DEFAULT 'USER';
CREATE INDEX "trades_buyer_source_idx" ON "trades"("buyer_source");
CREATE INDEX "trades_seller_source_idx" ON "trades"("seller_source");
-- 做市商配置表
CREATE TABLE "market_maker_configs" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"account_sequence" TEXT NOT NULL,
"cash_balance" DECIMAL(30,8) NOT NULL DEFAULT 0,
"share_balance" DECIMAL(30,8) NOT NULL DEFAULT 0,
"frozen_cash" DECIMAL(30,8) NOT NULL DEFAULT 0,
"frozen_shares" DECIMAL(30,8) NOT NULL DEFAULT 0,
"max_buy_ratio" DECIMAL(10,4) NOT NULL DEFAULT 0.05,
"min_interval_ms" INTEGER NOT NULL DEFAULT 1000,
"max_interval_ms" INTEGER NOT NULL DEFAULT 4000,
"price_strategy" TEXT NOT NULL DEFAULT 'TAKER',
"discount_rate" DECIMAL(10,4) NOT NULL DEFAULT 1.0,
"is_active" BOOLEAN NOT NULL DEFAULT false,
"last_run_at" TIMESTAMP(3),
"total_buy_count" INTEGER NOT NULL DEFAULT 0,
"total_buy_quantity" DECIMAL(30,8) NOT NULL DEFAULT 0,
"total_buy_amount" DECIMAL(30,8) NOT NULL DEFAULT 0,
"total_sell_count" INTEGER NOT NULL DEFAULT 0,
"total_sell_quantity" DECIMAL(30,8) NOT NULL DEFAULT 0,
"total_sell_amount" DECIMAL(30,8) NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "market_maker_configs_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "market_maker_configs_name_key" ON "market_maker_configs"("name");
CREATE UNIQUE INDEX "market_maker_configs_account_sequence_key" ON "market_maker_configs"("account_sequence");
-- 做市商分类账表
CREATE TABLE "market_maker_ledgers" (
"id" TEXT NOT NULL,
"market_maker_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"asset_type" TEXT NOT NULL,
"amount" DECIMAL(30,8) NOT NULL,
"balance_before" DECIMAL(30,8) NOT NULL,
"balance_after" DECIMAL(30,8) NOT NULL,
"trade_id" TEXT,
"trade_no" TEXT,
"order_id" TEXT,
"order_no" TEXT,
"counterparty_seq" TEXT,
"counterparty_id" TEXT,
"price" DECIMAL(30,18),
"quantity" DECIMAL(30,8),
"memo" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "market_maker_ledgers_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "market_maker_ledgers_market_maker_id_created_at_idx" ON "market_maker_ledgers"("market_maker_id", "created_at" DESC);
CREATE INDEX "market_maker_ledgers_type_idx" ON "market_maker_ledgers"("type");
CREATE INDEX "market_maker_ledgers_trade_no_idx" ON "market_maker_ledgers"("trade_no");
CREATE INDEX "market_maker_ledgers_order_no_idx" ON "market_maker_ledgers"("order_no");
CREATE INDEX "market_maker_ledgers_counterparty_seq_idx" ON "market_maker_ledgers"("counterparty_seq");
CREATE INDEX "market_maker_ledgers_created_at_idx" ON "market_maker_ledgers"("created_at" DESC);
ALTER TABLE "market_maker_ledgers" ADD CONSTRAINT "market_maker_ledgers_market_maker_id_fkey" FOREIGN KEY ("market_maker_id") REFERENCES "market_maker_configs"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- 做市商日统计表
CREATE TABLE "market_maker_daily_stats" (
"id" TEXT NOT NULL,
"market_maker_id" TEXT NOT NULL,
"date" DATE NOT NULL,
"buy_count" INTEGER NOT NULL DEFAULT 0,
"buy_quantity" DECIMAL(30,8) NOT NULL DEFAULT 0,
"buy_amount" DECIMAL(30,8) NOT NULL DEFAULT 0,
"avg_buy_price" DECIMAL(30,18) NOT NULL DEFAULT 0,
"sell_count" INTEGER NOT NULL DEFAULT 0,
"sell_quantity" DECIMAL(30,8) NOT NULL DEFAULT 0,
"sell_amount" DECIMAL(30,8) NOT NULL DEFAULT 0,
"avg_sell_price" DECIMAL(30,18) NOT NULL DEFAULT 0,
"realized_pnl" DECIMAL(30,8) NOT NULL DEFAULT 0,
"cash_balance_end" DECIMAL(30,8) NOT NULL DEFAULT 0,
"share_balance_end" DECIMAL(30,8) NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "market_maker_daily_stats_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "market_maker_daily_stats_market_maker_id_date_key" ON "market_maker_daily_stats"("market_maker_id", "date");
CREATE INDEX "market_maker_daily_stats_market_maker_id_date_idx" ON "market_maker_daily_stats"("market_maker_id", "date" DESC);

View File

@ -24,6 +24,8 @@ model TradingConfig {
isActive Boolean @default(false) @map("is_active")
// 是否启用买入功能(默认关闭)
buyEnabled Boolean @default(false) @map("buy_enabled")
// 是否启用深度显示(默认关闭)
depthEnabled Boolean @default(false) @map("depth_enabled")
// 启动时间
activatedAt DateTime? @map("activated_at")
createdAt DateTime @default(now()) @map("created_at")
@ -168,6 +170,9 @@ model Order {
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
burnMultiplier Decimal @default(0) @map("burn_multiplier") @db.Decimal(30, 18) // 销毁倍数
effectiveQuantity Decimal @default(0) @map("effective_quantity") @db.Decimal(30, 8) // 有效卖出量(含销毁)
// 订单来源
source String @default("USER") // USER, MARKET_MAKER, DEX_BOT, SYSTEM
sourceLabel String? @map("source_label") // 可读标签:如 "用户挂单", "做市商吃单"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cancelledAt DateTime?
@ -179,6 +184,7 @@ model Order {
@@index([accountSequence, status])
@@index([type, status, price])
@@index([createdAt(sort: Desc)])
@@index([source])
@@map("orders")
}
@ -195,12 +201,17 @@ model Trade {
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量quantity + burnQuantity
amount Decimal @db.Decimal(30, 8) // effectiveQty * price卖出交易额
// 交易来源标识
buyerSource String @default("USER") @map("buyer_source") // USER, MARKET_MAKER, DEX_BOT, SYSTEM
sellerSource String @default("USER") @map("seller_source") // USER, MARKET_MAKER, DEX_BOT, SYSTEM
createdAt DateTime @default(now()) @map("created_at")
buyOrder Order @relation(fields: [buyOrderId], references: [id])
@@index([buyerSequence])
@@index([sellerSequence])
@@index([buyerSource])
@@index([sellerSource])
@@index([createdAt(sort: Desc)])
@@map("trades")
}
@ -423,3 +434,152 @@ model ProcessedEvent {
@@index([processedAt])
@@map("processed_events")
}
// ==================== 做市商 ====================
// 做市商配置
model MarketMakerConfig {
id String @id @default(uuid())
name String @unique // 做市商名称,如 "MAIN_MARKET_MAKER"
accountSequence String @unique @map("account_sequence") // 做市商专用交易账户
// 资金配置从TradingAccount同步此处仅用于快速查询
cashBalance Decimal @default(0) @map("cash_balance") @db.Decimal(30, 8) // 资金池余额
shareBalance Decimal @default(0) @map("share_balance") @db.Decimal(30, 8) // 持有积分股余额
frozenCash Decimal @default(0) @map("frozen_cash") @db.Decimal(30, 8) // 冻结资金
frozenShares Decimal @default(0) @map("frozen_shares") @db.Decimal(30, 8) // 冻结积分股
// ============ 吃单策略配置(原有) ============
maxBuyRatio Decimal @default(0.05) @map("max_buy_ratio") @db.Decimal(10, 4) // 单次最大买入比例(资金池的百分比)
minIntervalMs Int @default(1000) @map("min_interval_ms") // 最小吃单间隔(毫秒)
maxIntervalMs Int @default(4000) @map("max_interval_ms") // 最大吃单间隔(毫秒)
priceStrategy String @default("TAKER") @map("price_strategy") // 价格策略: TAKER(按卖单价), MARKET(市场价), DISCOUNT(折扣价)
discountRate Decimal @default(1.0) @map("discount_rate") @db.Decimal(10, 4) // 折扣率仅DISCOUNT策略时生效
// ============ 双边挂单配置(深度做市) ============
// 挂单模式开关
makerEnabled Boolean @default(false) @map("maker_enabled") // 是否启用挂单模式(双边深度)
// 买单挂单配置
bidEnabled Boolean @default(true) @map("bid_enabled") // 是否挂买单
bidLevels Int @default(5) @map("bid_levels") // 买单档位数
bidSpread Decimal @default(0.01) @map("bid_spread") @db.Decimal(10, 4) // 买单价差相对市价如0.01表示1%
bidLevelSpacing Decimal @default(0.005) @map("bid_level_spacing") @db.Decimal(10, 4) // 买单档位间距如0.005表示0.5%
bidQuantityPerLevel Decimal @default(1000) @map("bid_quantity_per_level") @db.Decimal(30, 8) // 每档买单数量
// 卖单挂单配置
askEnabled Boolean @default(true) @map("ask_enabled") // 是否挂卖单
askLevels Int @default(5) @map("ask_levels") // 卖单档位数
askSpread Decimal @default(0.01) @map("ask_spread") @db.Decimal(10, 4) // 卖单价差(相对市价)
askLevelSpacing Decimal @default(0.005) @map("ask_level_spacing") @db.Decimal(10, 4) // 卖单档位间距
askQuantityPerLevel Decimal @default(1000) @map("ask_quantity_per_level") @db.Decimal(30, 8) // 每档卖单数量
// 挂单刷新配置
refreshIntervalMs Int @default(60000) @map("refresh_interval_ms") // 挂单刷新间隔(毫秒)
lastRefreshAt DateTime? @map("last_refresh_at") // 上次刷新时间
// ============ 运行状态 ============
isActive Boolean @default(false) @map("is_active") // 是否启用(吃单模式)
lastRunAt DateTime? @map("last_run_at") // 上次运行时间
// ============ 统计 ============
totalBuyCount Int @default(0) @map("total_buy_count") // 累计买入次数
totalBuyQuantity Decimal @default(0) @map("total_buy_quantity") @db.Decimal(30, 8) // 累计买入量
totalBuyAmount Decimal @default(0) @map("total_buy_amount") @db.Decimal(30, 8) // 累计买入金额
totalSellCount Int @default(0) @map("total_sell_count") // 累计卖出次数
totalSellQuantity Decimal @default(0) @map("total_sell_quantity") @db.Decimal(30, 8) // 累计卖出量
totalSellAmount Decimal @default(0) @map("total_sell_amount") @db.Decimal(30, 8) // 累计卖出金额
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
ledgers MarketMakerLedger[]
makerOrders MarketMakerOrder[]
@@map("market_maker_configs")
}
// 做市商挂单记录(跟踪做市商当前活跃的挂单)
model MarketMakerOrder {
id String @id @default(uuid())
marketMakerId String @map("market_maker_id")
orderId String @unique @map("order_id") // 关联的订单ID
orderNo String @unique @map("order_no") // 关联的订单号
side String // BID(买单), ASK(卖单)
level Int // 档位1=最优,越大越远离市价)
price Decimal @db.Decimal(30, 18) // 挂单价格
quantity Decimal @db.Decimal(30, 8) // 挂单数量
remainingQty Decimal @map("remaining_qty") @db.Decimal(30, 8) // 剩余数量
status String @default("ACTIVE") // ACTIVE, FILLED, CANCELLED
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
marketMaker MarketMakerConfig @relation(fields: [marketMakerId], references: [id])
@@index([marketMakerId, side, status])
@@index([status])
@@map("market_maker_orders")
}
// 做市商分类账(流水明细)
model MarketMakerLedger {
id String @id @default(uuid())
marketMakerId String @map("market_maker_id")
// 交易类型
type String // DEPOSIT(充值), WITHDRAW(提现), BUY(买入), SELL(卖出), FREEZE(冻结), UNFREEZE(解冻)
assetType String @map("asset_type") // CASH, SHARE
// 金额变动
amount Decimal @db.Decimal(30, 8)
balanceBefore Decimal @map("balance_before") @db.Decimal(30, 8)
balanceAfter Decimal @map("balance_after") @db.Decimal(30, 8)
// 关联信息
tradeId String? @map("trade_id") // 关联成交ID
tradeNo String? @map("trade_no") // 关联成交号
orderId String? @map("order_id") // 关联订单ID
orderNo String? @map("order_no") // 关联订单号
// 交易对手方
counterpartySeq String? @map("counterparty_seq") // 交易对手账户序列号
counterpartyId String? @map("counterparty_id") // 交易对手用户ID
// 价格信息仅BUY/SELL时
price Decimal? @db.Decimal(30, 18) // 成交价格
quantity Decimal? @db.Decimal(30, 8) // 成交数量
// 备注
memo String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
marketMaker MarketMakerConfig @relation(fields: [marketMakerId], references: [id])
@@index([marketMakerId, createdAt(sort: Desc)])
@@index([type])
@@index([tradeNo])
@@index([orderNo])
@@index([counterpartySeq])
@@index([createdAt(sort: Desc)])
@@map("market_maker_ledgers")
}
// 做市商日统计
model MarketMakerDailyStats {
id String @id @default(uuid())
marketMakerId String @map("market_maker_id")
date DateTime @db.Date
// 买入统计
buyCount Int @default(0) @map("buy_count")
buyQuantity Decimal @default(0) @map("buy_quantity") @db.Decimal(30, 8)
buyAmount Decimal @default(0) @map("buy_amount") @db.Decimal(30, 8)
avgBuyPrice Decimal @default(0) @map("avg_buy_price") @db.Decimal(30, 18)
// 卖出统计
sellCount Int @default(0) @map("sell_count")
sellQuantity Decimal @default(0) @map("sell_quantity") @db.Decimal(30, 8)
sellAmount Decimal @default(0) @map("sell_amount") @db.Decimal(30, 8)
avgSellPrice Decimal @default(0) @map("avg_sell_price") @db.Decimal(30, 18)
// 盈亏
realizedPnl Decimal @default(0) @map("realized_pnl") @db.Decimal(30, 8) // 已实现盈亏
// 余额快照
cashBalanceEnd Decimal @default(0) @map("cash_balance_end") @db.Decimal(30, 8)
shareBalanceEnd Decimal @default(0) @map("share_balance_end") @db.Decimal(30, 8)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([marketMakerId, date])
@@index([marketMakerId, date(sort: Desc)])
@@map("market_maker_daily_stats")
}

View File

@ -8,6 +8,7 @@ import { AdminController } from './controllers/admin.controller';
import { PriceController } from './controllers/price.controller';
import { BurnController } from './controllers/burn.controller';
import { AssetController } from './controllers/asset.controller';
import { MarketMakerController } from './controllers/market-maker.controller';
@Module({
imports: [ApplicationModule, InfrastructureModule],
@ -19,6 +20,7 @@ import { AssetController } from './controllers/asset.controller';
PriceController,
BurnController,
AssetController,
MarketMakerController,
],
})
export class ApiModule {}

View File

@ -8,6 +8,10 @@ class SetBuyEnabledDto {
enabled: boolean;
}
class SetDepthEnabledDto {
enabled: boolean;
}
@ApiTags('Admin')
@Controller('admin')
export class AdminController {
@ -165,4 +169,29 @@ export class AdminController {
message: '交易系统已关闭,每分钟销毁已暂停',
};
}
@Get('trading/depth-enabled')
@Public()
@ApiOperation({ summary: '获取深度显示开关状态' })
@ApiResponse({ status: 200, description: '返回深度显示是否启用' })
async getDepthEnabled() {
const config = await this.tradingConfigRepository.getConfig();
return {
enabled: config?.depthEnabled ?? false,
};
}
@Post('trading/depth-enabled')
@Public() // TODO: 生产环境应添加管理员权限验证
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '设置深度显示开关' })
@ApiResponse({ status: 200, description: '深度显示开关设置成功' })
async setDepthEnabled(@Body() dto: SetDepthEnabledDto) {
await this.tradingConfigRepository.setDepthEnabled(dto.enabled);
return {
success: true,
enabled: dto.enabled,
message: dto.enabled ? '深度显示已开启' : '深度显示已关闭',
};
}
}

View File

@ -0,0 +1,470 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
import { MarketMakerService, LedgerType, AssetType } from '../../application/services/market-maker.service';
import { Public } from '../../shared/guards/jwt-auth.guard';
// DTO 定义
class InitializeMarketMakerDto {
name?: string;
accountSequence: string;
initialCash?: string;
maxBuyRatio?: number;
minIntervalMs?: number;
maxIntervalMs?: number;
}
class DepositDto {
amount: string;
memo?: string;
}
class WithdrawDto {
amount: string;
memo?: string;
}
class UpdateConfigDto {
maxBuyRatio?: number;
minIntervalMs?: number;
maxIntervalMs?: number;
priceStrategy?: string;
discountRate?: number;
}
class UpdateMakerConfigDto {
bidEnabled?: boolean;
bidLevels?: number;
bidSpread?: number;
bidLevelSpacing?: number;
bidQuantityPerLevel?: string;
askEnabled?: boolean;
askLevels?: number;
askSpread?: number;
askLevelSpacing?: number;
askQuantityPerLevel?: string;
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);
if (!config) {
return {
success: false,
message: `做市商 ${name} 不存在`,
config: null,
};
}
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,
},
};
}
@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,
};
}
}

View File

@ -1,12 +1,18 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger';
import { PriceService } from '../../application/services/price.service';
import { MarketMakerService } from '../../application/services/market-maker.service';
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
import { Public } from '../../shared/guards/jwt-auth.guard';
@ApiTags('Price')
@Controller('price')
export class PriceController {
constructor(private readonly priceService: PriceService) {}
constructor(
private readonly priceService: PriceService,
private readonly marketMakerService: MarketMakerService,
private readonly tradingConfigRepository: TradingConfigRepository,
) {}
@Get('current')
@Public()
@ -61,4 +67,29 @@ export class PriceController {
) {
return this.priceService.getKlines(period, Math.min(limit, 500));
}
@Get('depth')
@Public()
@ApiOperation({ summary: '获取订单簿深度(买卖盘深度)' })
@ApiQuery({ name: 'levels', required: false, type: Number, description: '深度档位数默认10' })
async getDepth(@Query('levels') levels?: string) {
// 检查深度功能是否启用
const config = await this.tradingConfigRepository.getConfig();
if (!config || !config.depthEnabled) {
return {
success: true,
bids: [],
asks: [],
timestamp: Date.now(),
message: '深度功能未启用',
};
}
const depth = await this.marketMakerService.getDepth(levels ? parseInt(levels, 10) : 10);
return {
success: true,
...depth,
};
}
}

View File

@ -6,6 +6,7 @@ import { TransferService } from './services/transfer.service';
import { PriceService } from './services/price.service';
import { BurnService } from './services/burn.service';
import { AssetService } from './services/asset.service';
import { MarketMakerService } from './services/market-maker.service';
import { OutboxScheduler } from './schedulers/outbox.scheduler';
import { BurnScheduler } from './schedulers/burn.scheduler';
@ -18,10 +19,11 @@ import { BurnScheduler } from './schedulers/burn.scheduler';
AssetService,
OrderService,
TransferService,
MarketMakerService,
// Schedulers
OutboxScheduler,
BurnScheduler,
],
exports: [OrderService, TransferService, PriceService, BurnService, AssetService],
exports: [OrderService, TransferService, PriceService, BurnService, AssetService, MarketMakerService],
})
export class ApplicationModule {}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import { CirculationPoolRepository } from '../../infrastructure/persistence/repo
import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
import { RedisService } from '../../infrastructure/redis/redis.service';
import { OrderAggregate, OrderType, OrderStatus } from '../../domain/aggregates/order.aggregate';
import { OrderAggregate, OrderType, OrderStatus, OrderSource } from '../../domain/aggregates/order.aggregate';
import { TradingAccountAggregate } from '../../domain/aggregates/trading-account.aggregate';
import { MatchingEngineService } from '../../domain/services/matching-engine.service';
import { Money } from '../../domain/value-objects/money.vo';
@ -40,6 +40,8 @@ export class OrderService {
type: OrderType,
price: string,
quantity: string,
source: OrderSource = OrderSource.USER,
sourceLabel?: string,
): Promise<{ orderId: string; orderNo: string; status: OrderStatus; filledQuantity: string }> {
const lockValue = await this.redis.acquireLock(`order:create:${accountSequence}`, 10);
if (!lockValue) {
@ -72,7 +74,7 @@ export class OrderService {
const orderNo = this.generateOrderNo();
// 创建订单
const order = OrderAggregate.create(orderNo, accountSequence, type, priceAmount, quantityAmount);
const order = OrderAggregate.create(orderNo, accountSequence, type, priceAmount, quantityAmount, source, sourceLabel);
// 冻结资产
if (type === OrderType.BUY) {
@ -176,7 +178,7 @@ export class OrderService {
// 计算交易额 = 有效数量 × 价格
const tradeAmount = new Money(effectiveQuantity.value.times(match.trade.price.value));
// 保存成交记录(包含销毁信息
// 保存成交记录(包含销毁信息和来源标识
await this.prisma.trade.create({
data: {
tradeNo: match.trade.tradeNo,
@ -189,6 +191,8 @@ export class OrderService {
burnQuantity: burnQuantity.value,
effectiveQty: effectiveQuantity.value,
amount: tradeAmount.value,
buyerSource: match.buyOrder.source,
sellerSource: match.sellOrder.source,
},
});

View File

@ -13,6 +13,13 @@ export enum OrderStatus {
CANCELLED = 'CANCELLED',
}
export enum OrderSource {
USER = 'USER',
MARKET_MAKER = 'MARKET_MAKER',
DEX_BOT = 'DEX_BOT',
SYSTEM = 'SYSTEM',
}
export interface TradeInfo {
tradeNo: string;
counterpartyOrderId: string;
@ -29,6 +36,8 @@ export class OrderAggregate {
private _accountSequence: string;
private _type: OrderType;
private _status: OrderStatus;
private _source: OrderSource;
private _sourceLabel: string | null;
private _price: Money;
private _quantity: Money;
private _filledQuantity: Money;
@ -46,6 +55,8 @@ export class OrderAggregate {
type: OrderType,
price: Money,
quantity: Money,
source: OrderSource = OrderSource.USER,
sourceLabel: string | null = null,
id: string | null = null,
) {
this._id = id;
@ -53,6 +64,8 @@ export class OrderAggregate {
this._accountSequence = accountSequence;
this._type = type;
this._status = OrderStatus.PENDING;
this._source = source;
this._sourceLabel = sourceLabel;
this._price = price;
this._quantity = quantity;
this._filledQuantity = Money.zero();
@ -68,6 +81,8 @@ export class OrderAggregate {
type: OrderType,
price: Money,
quantity: Money,
source: OrderSource = OrderSource.USER,
sourceLabel: string | null = null,
): OrderAggregate {
if (price.isZero()) {
throw new Error('Price cannot be zero');
@ -75,7 +90,7 @@ export class OrderAggregate {
if (quantity.isZero()) {
throw new Error('Quantity cannot be zero');
}
return new OrderAggregate(orderNo, accountSequence, type, price, quantity);
return new OrderAggregate(orderNo, accountSequence, type, price, quantity, source, sourceLabel);
}
static reconstitute(props: {
@ -84,6 +99,8 @@ export class OrderAggregate {
accountSequence: string;
type: OrderType;
status: OrderStatus;
source: OrderSource;
sourceLabel: string | null;
price: Money;
quantity: Money;
filledQuantity: Money;
@ -100,6 +117,8 @@ export class OrderAggregate {
props.type,
props.price,
props.quantity,
props.source,
props.sourceLabel,
props.id,
);
order._status = props.status;
@ -119,6 +138,8 @@ export class OrderAggregate {
get accountSequence(): string { return this._accountSequence; }
get type(): OrderType { return this._type; }
get status(): OrderStatus { return this._status; }
get source(): OrderSource { return this._source; }
get sourceLabel(): string | null { return this._sourceLabel; }
get price(): Money { return this._price; }
get quantity(): Money { return this._quantity; }
get filledQuantity(): Money { return this._filledQuantity; }

View File

@ -288,6 +288,63 @@ export class TradingAccountAggregate {
});
}
// 提现现金
withdraw(amount: Money, referenceId: string): void {
if (this.availableCash.isLessThan(amount)) {
throw new Error('Insufficient available cash for withdrawal');
}
const balanceBefore = this._cashBalance;
this._cashBalance = this._cashBalance.subtract(amount);
this._pendingTransactions.push({
type: TradingTransactionType.WITHDRAW,
assetType: AssetType.CASH,
amount,
balanceBefore,
balanceAfter: this._cashBalance,
referenceId,
referenceType: 'WITHDRAW',
description: '提现',
createdAt: new Date(),
});
}
// 充值积分股(做市商用)
depositShares(amount: Money, referenceId: string): void {
const balanceBefore = this._shareBalance;
this._shareBalance = this._shareBalance.add(amount);
this._pendingTransactions.push({
type: TradingTransactionType.DEPOSIT,
assetType: AssetType.SHARE,
amount,
balanceBefore,
balanceAfter: this._shareBalance,
referenceId,
referenceType: 'DEPOSIT',
description: '积分股充值',
createdAt: new Date(),
});
}
// 提取积分股(做市商用)
withdrawShares(amount: Money, referenceId: string): void {
if (this.availableShares.isLessThan(amount)) {
throw new Error('Insufficient available shares for withdrawal');
}
const balanceBefore = this._shareBalance;
this._shareBalance = this._shareBalance.subtract(amount);
this._pendingTransactions.push({
type: TradingTransactionType.WITHDRAW,
assetType: AssetType.SHARE,
amount,
balanceBefore,
balanceAfter: this._shareBalance,
referenceId,
referenceType: 'WITHDRAW',
description: '积分股提取',
createdAt: new Date(),
});
}
clearPendingTransactions(): void {
this._pendingTransactions = [];
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { OrderAggregate, OrderType, OrderStatus } from '../../../domain/aggregates/order.aggregate';
import { OrderAggregate, OrderType, OrderStatus, OrderSource } from '../../../domain/aggregates/order.aggregate';
import { Money } from '../../../domain/value-objects/money.vo';
@Injectable()
@ -25,6 +25,8 @@ export class OrderRepository {
accountSequence: aggregate.accountSequence,
type: aggregate.type,
status: aggregate.status,
source: aggregate.source,
sourceLabel: aggregate.sourceLabel,
price: aggregate.price.value,
quantity: aggregate.quantity.value,
filledQuantity: aggregate.filledQuantity.value,
@ -60,6 +62,8 @@ export class OrderRepository {
accountSequence: aggregate.accountSequence,
type: aggregate.type,
status: aggregate.status,
source: aggregate.source,
sourceLabel: aggregate.sourceLabel,
price: aggregate.price.value,
quantity: aggregate.quantity.value,
filledQuantity: aggregate.filledQuantity.value,
@ -164,6 +168,51 @@ export class OrderRepository {
};
}
/**
*
*/
async findOldestPendingSellOrder(): Promise<OrderAggregate | null> {
const record = await this.prisma.order.findFirst({
where: {
type: OrderType.SELL,
status: { in: [OrderStatus.PENDING, OrderStatus.PARTIAL] },
},
orderBy: { createdAt: 'asc' },
});
if (!record) return null;
return this.toDomain(record);
}
/**
*
*/
async findBySource(
source: OrderSource,
options?: { type?: OrderType; status?: OrderStatus; page?: number; pageSize?: number },
): Promise<{ data: OrderAggregate[]; total: number }> {
const where: any = { source };
if (options?.type) where.type = options.type;
if (options?.status) where.status = options.status;
const page = options?.page ?? 1;
const pageSize = options?.pageSize ?? 50;
const [records, total] = await Promise.all([
this.prisma.order.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.order.count({ where }),
]);
return {
data: records.map((r) => this.toDomain(r)),
total,
};
}
private toDomain(record: any): OrderAggregate {
return OrderAggregate.reconstitute({
id: record.id,
@ -171,6 +220,8 @@ export class OrderRepository {
accountSequence: record.accountSequence,
type: record.type as OrderType,
status: record.status as OrderStatus,
source: (record.source as OrderSource) || OrderSource.USER,
sourceLabel: record.sourceLabel,
price: new Money(record.price),
quantity: new Money(record.quantity),
filledQuantity: new Money(record.filledQuantity),

View File

@ -11,6 +11,7 @@ export interface TradingConfigEntity {
minuteBurnRate: Money;
isActive: boolean;
buyEnabled: boolean;
depthEnabled: boolean;
activatedAt: Date | null;
createdAt: Date;
updatedAt: Date;
@ -98,6 +99,18 @@ export class TradingConfigRepository {
});
}
async setDepthEnabled(enabled: boolean): Promise<void> {
const config = await this.prisma.tradingConfig.findFirst();
if (!config) {
throw new Error('Trading config not initialized');
}
await this.prisma.tradingConfig.update({
where: { id: config.id },
data: { depthEnabled: enabled },
});
}
private toDomain(record: any): TradingConfigEntity {
return {
id: record.id,
@ -107,6 +120,7 @@ export class TradingConfigRepository {
minuteBurnRate: new Money(record.minuteBurnRate),
isActive: record.isActive,
buyEnabled: record.buyEnabled ?? false,
depthEnabled: record.depthEnabled ?? false,
activatedAt: record.activatedAt,
createdAt: record.createdAt,
updatedAt: record.updatedAt,

View File

@ -0,0 +1,648 @@
'use client';
import { useState } from 'react';
import { PageHeader } from '@/components/layout/page-header';
import {
useMarketMakerConfig,
useInitializeMarketMaker,
useDepositCash,
useWithdrawCash,
useDepositShares,
useWithdrawShares,
useStartTaker,
useStopTaker,
useTakeOrder,
useStartMaker,
useStopMaker,
useRefreshOrders,
useCancelAllOrders,
useDepth,
useDepthEnabled,
useSetDepthEnabled,
} from '@/features/market-maker';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
Play,
Pause,
AlertCircle,
CheckCircle2,
RefreshCw,
Wallet,
TrendingUp,
TrendingDown,
BarChart3,
Zap,
PlusCircle,
MinusCircle,
} from 'lucide-react';
export default function MarketMakerPage() {
const { data: configData, isLoading: configLoading, refetch } = useMarketMakerConfig();
const { data: depthData, isLoading: depthLoading } = useDepth(10);
const { data: depthEnabled, isLoading: depthEnabledLoading } = useDepthEnabled();
const initializeMutation = useInitializeMarketMaker();
const depositCashMutation = useDepositCash();
const withdrawCashMutation = useWithdrawCash();
const depositSharesMutation = useDepositShares();
const withdrawSharesMutation = useWithdrawShares();
const startTakerMutation = useStartTaker();
const stopTakerMutation = useStopTaker();
const takeOrderMutation = useTakeOrder();
const startMakerMutation = useStartMaker();
const stopMakerMutation = useStopMaker();
const refreshOrdersMutation = useRefreshOrders();
const cancelAllOrdersMutation = useCancelAllOrders();
const setDepthEnabledMutation = useSetDepthEnabled();
const [initAccountSeq, setInitAccountSeq] = useState('MM001');
const [depositCashAmount, setDepositCashAmount] = useState('');
const [withdrawCashAmount, setWithdrawCashAmount] = useState('');
const [depositSharesAmount, setDepositSharesAmount] = useState('');
const [withdrawSharesAmount, setWithdrawSharesAmount] = useState('');
const config = configData?.config;
const formatNumber = (value: string | undefined, decimals: number = 2) => {
if (!value) return '0';
const num = parseFloat(value);
if (isNaN(num)) return '0';
return num.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<PageHeader title="做市商管理" description="管理做市商配置、资金和深度挂单" />
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 未初始化状态 */}
{!configLoading && !config && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Input
value={initAccountSeq}
onChange={(e) => setInitAccountSeq(e.target.value)}
placeholder="做市商专用账户序列号"
/>
</div>
<Button
onClick={() => initializeMutation.mutate({ accountSequence: initAccountSeq })}
disabled={initializeMutation.isPending || !initAccountSeq}
>
{initializeMutation.isPending ? '初始化中...' : '初始化做市商'}
</Button>
</div>
</CardContent>
</Card>
)}
{configLoading && (
<div className="space-y-6">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-48 w-full" />
</div>
)}
{config && (
<>
{/* 资金状态卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 现金余额 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Wallet className="h-5 w-5 text-green-500" />
</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{formatNumber(config.cashBalance, 2)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold text-green-500">{formatNumber(config.availableCash, 2)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg text-muted-foreground">{formatNumber(config.frozenCash, 2)}</p>
</div>
</div>
<div className="flex gap-2 pt-4 border-t">
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<PlusCircle className="h-4 w-4 mr-1" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="py-4">
<Label></Label>
<Input
type="number"
value={depositCashAmount}
onChange={(e) => setDepositCashAmount(e.target.value)}
placeholder="请输入金额"
/>
</div>
<DialogFooter>
<Button
onClick={() => {
depositCashMutation.mutate({ amount: depositCashAmount });
setDepositCashAmount('');
}}
disabled={depositCashMutation.isPending || !depositCashAmount}
>
{depositCashMutation.isPending ? '处理中...' : '确认充值'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<MinusCircle className="h-4 w-4 mr-1" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="py-4">
<Label></Label>
<Input
type="number"
value={withdrawCashAmount}
onChange={(e) => setWithdrawCashAmount(e.target.value)}
placeholder="请输入金额"
/>
</div>
<DialogFooter>
<Button
onClick={() => {
withdrawCashMutation.mutate({ amount: withdrawCashAmount });
setWithdrawCashAmount('');
}}
disabled={withdrawCashMutation.isPending || !withdrawCashAmount}
>
{withdrawCashMutation.isPending ? '处理中...' : '确认提现'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
{/* 积分股余额 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-blue-500" />
</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{formatNumber(config.shareBalance, 2)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold text-blue-500">{formatNumber(config.availableShares, 2)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg text-muted-foreground">{formatNumber(config.frozenShares, 2)}</p>
</div>
</div>
<div className="flex gap-2 pt-4 border-t">
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<PlusCircle className="h-4 w-4 mr-1" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="py-4">
<Label></Label>
<Input
type="number"
value={depositSharesAmount}
onChange={(e) => setDepositSharesAmount(e.target.value)}
placeholder="请输入数量"
/>
</div>
<DialogFooter>
<Button
onClick={() => {
depositSharesMutation.mutate({ amount: depositSharesAmount });
setDepositSharesAmount('');
}}
disabled={depositSharesMutation.isPending || !depositSharesAmount}
>
{depositSharesMutation.isPending ? '处理中...' : '确认充值'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<MinusCircle className="h-4 w-4 mr-1" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="py-4">
<Label></Label>
<Input
type="number"
value={withdrawSharesAmount}
onChange={(e) => setWithdrawSharesAmount(e.target.value)}
placeholder="请输入数量"
/>
</div>
<DialogFooter>
<Button
onClick={() => {
withdrawSharesMutation.mutate({ amount: withdrawSharesAmount });
setWithdrawSharesAmount('');
}}
disabled={withdrawSharesMutation.isPending || !withdrawSharesAmount}
>
{withdrawSharesMutation.isPending ? '处理中...' : '确认提取'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 运行模式控制 */}
<Tabs defaultValue="taker">
<TabsList>
<TabsTrigger value="taker"></TabsTrigger>
<TabsTrigger value="maker"></TabsTrigger>
<TabsTrigger value="depth"></TabsTrigger>
</TabsList>
{/* 吃单模式 */}
<TabsContent value="taker">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Zap className="h-5 w-5" />
</CardTitle>
<CardDescription>Taker </CardDescription>
</div>
{config.isActive ? (
<Badge variant="default" className="bg-green-500">
<CheckCircle2 className="h-3 w-3 mr-1" />
</Badge>
) : (
<Badge variant="secondary">
<Pause className="h-3 w-3 mr-1" />
</Badge>
)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{formatNumber(String(parseFloat(config.maxBuyRatio) * 100), 2)}%</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{config.minIntervalMs}ms</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{config.maxIntervalMs}ms</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{config.priceStrategy}</p>
</div>
</div>
<div className="flex gap-2 pt-4 border-t">
{config.isActive ? (
<Button
variant="destructive"
onClick={() => stopTakerMutation.mutate()}
disabled={stopTakerMutation.isPending}
>
<Pause className="h-4 w-4 mr-2" />
{stopTakerMutation.isPending ? '停止中...' : '停止吃单'}
</Button>
) : (
<Button
onClick={() => startTakerMutation.mutate()}
disabled={startTakerMutation.isPending}
>
<Play className="h-4 w-4 mr-2" />
{startTakerMutation.isPending ? '启动中...' : '启动吃单'}
</Button>
)}
<Button
variant="outline"
onClick={() => takeOrderMutation.mutate()}
disabled={takeOrderMutation.isPending}
>
<Zap className="h-4 w-4 mr-2" />
{takeOrderMutation.isPending ? '执行中...' : '手动吃单'}
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 挂单模式 */}
<TabsContent value="maker">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
</CardTitle>
<CardDescription>Maker </CardDescription>
</div>
{config.makerEnabled ? (
<Badge variant="default" className="bg-green-500">
<CheckCircle2 className="h-3 w-3 mr-1" />
</Badge>
) : (
<Badge variant="secondary">
<Pause className="h-3 w-3 mr-1" />
</Badge>
)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{config.bidLevels || 5}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{formatNumber(String(parseFloat(config.bidSpread || '0.01') * 100), 2)}%</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{formatNumber(config.bidQuantityPerLevel || '1000', 0)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{config.askLevels || 5}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{formatNumber(String(parseFloat(config.askSpread || '0.01') * 100), 2)}%</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="font-semibold">{formatNumber(config.askQuantityPerLevel || '1000', 0)}</p>
</div>
</div>
<div className="flex gap-2 pt-4 border-t">
{config.makerEnabled ? (
<Button
variant="destructive"
onClick={() => stopMakerMutation.mutate()}
disabled={stopMakerMutation.isPending}
>
<Pause className="h-4 w-4 mr-2" />
{stopMakerMutation.isPending ? '停止中...' : '停止挂单'}
</Button>
) : (
<Button
onClick={() => startMakerMutation.mutate()}
disabled={startMakerMutation.isPending}
>
<Play className="h-4 w-4 mr-2" />
{startMakerMutation.isPending ? '启动中...' : '启动挂单'}
</Button>
)}
<Button
variant="outline"
onClick={() => refreshOrdersMutation.mutate()}
disabled={refreshOrdersMutation.isPending}
>
<RefreshCw className="h-4 w-4 mr-2" />
{refreshOrdersMutation.isPending ? '刷新中...' : '刷新挂单'}
</Button>
<Button
variant="outline"
onClick={() => cancelAllOrdersMutation.mutate()}
disabled={cancelAllOrdersMutation.isPending}
>
{cancelAllOrdersMutation.isPending ? '取消中...' : '取消所有挂单'}
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 深度显示控制 */}
<TabsContent value="depth">
<div className="space-y-6">
{/* 深度开关 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription>App端是否显示买卖深度</CardDescription>
</div>
{depthEnabledLoading ? (
<Skeleton className="h-6 w-16" />
) : depthEnabled?.enabled ? (
<Badge variant="default" className="bg-green-500">
<CheckCircle2 className="h-3 w-3 mr-1" />
</Badge>
) : (
<Badge variant="secondary">
<Pause className="h-3 w-3 mr-1" />
</Badge>
)}
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
{depthEnabled?.enabled ? '用户可以在App中查看买卖深度' : '深度功能已关闭,用户无法查看'}
</p>
</div>
<Switch
checked={depthEnabled?.enabled ?? false}
onCheckedChange={(checked) => setDepthEnabledMutation.mutate(checked)}
disabled={setDepthEnabledMutation.isPending || depthEnabledLoading}
/>
</div>
</CardContent>
</Card>
{/* 当前深度 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
{depthLoading ? (
<Skeleton className="h-64 w-full" />
) : (
<div className="grid grid-cols-2 gap-6">
{/* 买单深度 */}
<div>
<h4 className="font-medium text-green-500 mb-2 flex items-center gap-1">
<TrendingUp className="h-4 w-4" />
</h4>
{depthData?.bids?.length ? (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{depthData.bids.map((bid, idx) => (
<TableRow key={idx}>
<TableCell className="font-mono text-green-500">{formatNumber(bid.price, 8)}</TableCell>
<TableCell className="font-mono">{formatNumber(bid.quantity, 2)}</TableCell>
<TableCell className="font-mono text-muted-foreground">{formatNumber(bid.total, 2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-center text-muted-foreground py-4"></p>
)}
</div>
{/* 卖单深度 */}
<div>
<h4 className="font-medium text-red-500 mb-2 flex items-center gap-1">
<TrendingDown className="h-4 w-4" />
</h4>
{depthData?.asks?.length ? (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{depthData.asks.map((ask, idx) => (
<TableRow key={idx}>
<TableCell className="font-mono text-red-500">{formatNumber(ask.price, 8)}</TableCell>
<TableCell className="font-mono">{formatNumber(ask.quantity, 2)}</TableCell>
<TableCell className="font-mono text-muted-foreground">{formatNumber(ask.total, 2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-center text-muted-foreground py-4"></p>
)}
</div>
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</>
)}
</div>
);
}

View File

@ -14,6 +14,7 @@ import {
ChevronLeft,
ChevronRight,
ArrowLeftRight,
Bot,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
@ -21,6 +22,7 @@ const menuItems = [
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
{ name: '用户管理', href: '/users', icon: Users },
{ name: '交易管理', href: '/trading', icon: ArrowLeftRight },
{ name: '做市商管理', href: '/market-maker', icon: Bot },
{ name: '配置管理', href: '/configs', icon: Settings },
{ name: '系统账户', href: '/system-accounts', icon: Building2 },
{ name: '报表统计', href: '/reports', icon: FileBarChart },

View File

@ -0,0 +1,268 @@
import axios from 'axios';
const tradingBaseURL = '/api/trading';
const tradingClient = axios.create({
baseURL: tradingBaseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
tradingClient.interceptors.request.use(
(config) => {
const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
tradingClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('admin_token');
if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
export interface MarketMakerConfig {
id: string;
name: string;
accountSequence: string;
cashBalance: string;
shareBalance: string;
frozenCash: string;
frozenShares: string;
availableCash: string;
availableShares: string;
maxBuyRatio: string;
minIntervalMs: number;
maxIntervalMs: number;
priceStrategy: string;
discountRate: string;
isActive: boolean;
makerEnabled?: boolean;
bidEnabled?: boolean;
bidLevels?: number;
bidSpread?: string;
bidLevelSpacing?: string;
bidQuantityPerLevel?: string;
askEnabled?: boolean;
askLevels?: number;
askSpread?: string;
askLevelSpacing?: string;
askQuantityPerLevel?: string;
refreshIntervalMs?: number;
lastRefreshAt?: string;
}
export interface MarketMakerOrder {
id: string;
orderNo: string;
side: 'BID' | 'ASK';
level: number;
price: string;
quantity: string;
remainingQty: string;
status: string;
createdAt: string;
}
export interface DepthLevel {
price: string;
quantity: string;
total: string;
}
export interface DepthData {
bids: DepthLevel[];
asks: DepthLevel[];
timestamp: number;
}
export const marketMakerApi = {
// 获取做市商配置
getConfig: async (name: string = 'MAIN_MARKET_MAKER'): Promise<{ success: boolean; config: MarketMakerConfig | null }> => {
const response = await tradingClient.get(`/admin/market-maker/${name}/config`);
return response.data;
},
// 初始化做市商
initialize: async (data: {
name?: string;
accountSequence: string;
initialCash?: string;
maxBuyRatio?: number;
minIntervalMs?: number;
maxIntervalMs?: number;
}): Promise<{ success: boolean; message: string; config: MarketMakerConfig }> => {
const response = await tradingClient.post('/admin/market-maker/initialize', data);
return response.data;
},
// 更新做市商配置
updateConfig: async (name: string, data: {
maxBuyRatio?: number;
minIntervalMs?: number;
maxIntervalMs?: number;
priceStrategy?: string;
discountRate?: number;
}): Promise<{ success: boolean; message: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/config`, data);
return response.data;
},
// 充值资金
deposit: async (name: string, amount: string, memo?: string): Promise<{ success: boolean; message: string; newBalance: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/deposit`, { amount, memo });
return response.data;
},
// 提现资金
withdraw: async (name: string, amount: string, memo?: string): Promise<{ success: boolean; message: string; newBalance: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/withdraw`, { amount, memo });
return response.data;
},
// 充值积分股
depositShares: async (name: string, amount: string, memo?: string): Promise<{ success: boolean; message: string; newShareBalance: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/deposit-shares`, { amount, memo });
return response.data;
},
// 提取积分股
withdrawShares: async (name: string, amount: string, memo?: string): Promise<{ success: boolean; message: string; newShareBalance: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/withdraw-shares`, { amount, memo });
return response.data;
},
// 启动吃单模式
start: async (name: string): Promise<{ success: boolean; message: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/start`);
return response.data;
},
// 停止吃单模式
stop: async (name: string): Promise<{ success: boolean; message: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/stop`);
return response.data;
},
// 手动执行一次吃单
takeOrder: async (name: string): Promise<{
success: boolean;
message: string;
tradeInfo?: {
orderNo: string;
sellOrderNo: string;
price: string;
quantity: string;
amount: string;
};
}> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/take-order`);
return response.data;
},
// 启动挂单模式
startMaker: async (name: string): Promise<{ success: boolean; message: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/start-maker`);
return response.data;
},
// 停止挂单模式
stopMaker: async (name: string): Promise<{ success: boolean; message: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/stop-maker`);
return response.data;
},
// 刷新挂单
refreshOrders: async (name: string): Promise<{
success: boolean;
message: string;
bidOrders?: number;
askOrders?: number;
}> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/refresh-orders`);
return response.data;
},
// 取消所有挂单
cancelAllOrders: async (name: string): Promise<{ success: boolean; message: string; cancelledCount: number }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/cancel-all-orders`);
return response.data;
},
// 更新挂单配置
updateMakerConfig: async (name: string, data: {
bidEnabled?: boolean;
bidLevels?: number;
bidSpread?: number;
bidLevelSpacing?: number;
bidQuantityPerLevel?: string;
askEnabled?: boolean;
askLevels?: number;
askSpread?: number;
askLevelSpacing?: number;
askQuantityPerLevel?: string;
refreshIntervalMs?: number;
}): Promise<{ success: boolean; message: string }> => {
const response = await tradingClient.post(`/admin/market-maker/${name}/maker-config`, data);
return response.data;
},
// 获取做市商挂单列表
getMakerOrders: async (name: string, params?: {
side?: 'BID' | 'ASK';
status?: 'ACTIVE' | 'FILLED' | 'CANCELLED';
page?: number;
pageSize?: number;
}): Promise<{ success: boolean; data: MarketMakerOrder[]; total: number }> => {
const searchParams = new URLSearchParams();
if (params?.side) searchParams.append('side', params.side);
if (params?.status) searchParams.append('status', params.status);
if (params?.page) searchParams.append('page', params.page.toString());
if (params?.pageSize) searchParams.append('pageSize', params.pageSize.toString());
const response = await tradingClient.get(`/admin/market-maker/${name}/maker-orders?${searchParams.toString()}`);
return response.data;
},
// 获取深度数据
getDepth: async (levels?: number): Promise<{ success: boolean } & DepthData> => {
const params = levels ? `?levels=${levels}` : '';
const response = await tradingClient.get(`/admin/market-maker/depth${params}`);
return response.data;
},
// 获取统计信息
getStats: async (name: string): Promise<{
success: boolean;
config: Partial<MarketMakerConfig>;
recentTrades: any[];
dailyStats: any[];
}> => {
const response = await tradingClient.get(`/admin/market-maker/${name}/stats`);
return response.data;
},
// 获取深度功能开关状态
getDepthEnabled: async (): Promise<{ enabled: boolean }> => {
const response = await tradingClient.get('/admin/trading/depth-enabled');
return response.data;
},
// 设置深度功能开关
setDepthEnabled: async (enabled: boolean): Promise<{ success: boolean; enabled: boolean; message: string }> => {
const response = await tradingClient.post('/admin/trading/depth-enabled', { enabled });
return response.data;
},
};

View File

@ -0,0 +1,299 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { marketMakerApi } from '../api/market-maker.api';
import { toast } from '@/lib/hooks/use-toast';
const MARKET_MAKER_NAME = 'MAIN_MARKET_MAKER';
export function useMarketMakerConfig() {
return useQuery({
queryKey: ['marketMaker', 'config', MARKET_MAKER_NAME],
queryFn: () => marketMakerApi.getConfig(MARKET_MAKER_NAME),
refetchInterval: 10000,
});
}
export function useInitializeMarketMaker() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
accountSequence: string;
initialCash?: string;
maxBuyRatio?: number;
}) => marketMakerApi.initialize({ ...data, name: MARKET_MAKER_NAME }),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '初始化失败', variant: 'destructive' });
},
});
}
export function useUpdateMarketMakerConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
maxBuyRatio?: number;
minIntervalMs?: number;
maxIntervalMs?: number;
priceStrategy?: string;
discountRate?: number;
}) => marketMakerApi.updateConfig(MARKET_MAKER_NAME, data),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '更新失败', variant: 'destructive' });
},
});
}
export function useDepositCash() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ amount, memo }: { amount: string; memo?: string }) =>
marketMakerApi.deposit(MARKET_MAKER_NAME, amount, memo),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '充值失败', variant: 'destructive' });
},
});
}
export function useWithdrawCash() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ amount, memo }: { amount: string; memo?: string }) =>
marketMakerApi.withdraw(MARKET_MAKER_NAME, amount, memo),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '提现失败', variant: 'destructive' });
},
});
}
export function useDepositShares() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ amount, memo }: { amount: string; memo?: string }) =>
marketMakerApi.depositShares(MARKET_MAKER_NAME, amount, memo),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '充值失败', variant: 'destructive' });
},
});
}
export function useWithdrawShares() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ amount, memo }: { amount: string; memo?: string }) =>
marketMakerApi.withdrawShares(MARKET_MAKER_NAME, amount, memo),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '提现失败', variant: 'destructive' });
},
});
}
export function useStartTaker() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.start(MARKET_MAKER_NAME),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '启动失败', variant: 'destructive' });
},
});
}
export function useStopTaker() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.stop(MARKET_MAKER_NAME),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '停止失败', variant: 'destructive' });
},
});
}
export function useTakeOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.takeOrder(MARKET_MAKER_NAME),
onSuccess: (data) => {
if (data.success && data.tradeInfo) {
toast({
title: '吃单成功',
description: `成交价格: ${data.tradeInfo.price}, 数量: ${data.tradeInfo.quantity}`,
});
} else {
toast({ title: '提示', description: data.message });
}
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '吃单失败', variant: 'destructive' });
},
});
}
export function useStartMaker() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.startMaker(MARKET_MAKER_NAME),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '启动失败', variant: 'destructive' });
},
});
}
export function useStopMaker() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.stopMaker(MARKET_MAKER_NAME),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '停止失败', variant: 'destructive' });
},
});
}
export function useRefreshOrders() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.refreshOrders(MARKET_MAKER_NAME),
onSuccess: (data) => {
if (data.success) {
toast({
title: '刷新成功',
description: `买单: ${data.bidOrders || 0}, 卖单: ${data.askOrders || 0}`,
});
} else {
toast({ title: '提示', description: data.message });
}
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '刷新失败', variant: 'destructive' });
},
});
}
export function useCancelAllOrders() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => marketMakerApi.cancelAllOrders(MARKET_MAKER_NAME),
onSuccess: (data) => {
toast({ title: '成功', description: `已取消 ${data.cancelledCount} 个挂单` });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '取消失败', variant: 'destructive' });
},
});
}
export function useUpdateMakerConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: {
bidEnabled?: boolean;
bidLevels?: number;
bidSpread?: number;
bidLevelSpacing?: number;
bidQuantityPerLevel?: string;
askEnabled?: boolean;
askLevels?: number;
askSpread?: number;
askLevelSpacing?: number;
askQuantityPerLevel?: string;
refreshIntervalMs?: number;
}) => marketMakerApi.updateMakerConfig(MARKET_MAKER_NAME, data),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['marketMaker'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '更新失败', variant: 'destructive' });
},
});
}
export function useMakerOrders(params?: {
side?: 'BID' | 'ASK';
status?: 'ACTIVE' | 'FILLED' | 'CANCELLED';
page?: number;
pageSize?: number;
}) {
return useQuery({
queryKey: ['marketMaker', 'orders', MARKET_MAKER_NAME, params],
queryFn: () => marketMakerApi.getMakerOrders(MARKET_MAKER_NAME, params),
refetchInterval: 10000,
});
}
export function useDepth(levels?: number) {
return useQuery({
queryKey: ['marketMaker', 'depth', levels],
queryFn: () => marketMakerApi.getDepth(levels),
refetchInterval: 5000,
});
}
export function useMarketMakerStats() {
return useQuery({
queryKey: ['marketMaker', 'stats', MARKET_MAKER_NAME],
queryFn: () => marketMakerApi.getStats(MARKET_MAKER_NAME),
refetchInterval: 10000,
});
}
export function useDepthEnabled() {
return useQuery({
queryKey: ['trading', 'depthEnabled'],
queryFn: () => marketMakerApi.getDepthEnabled(),
});
}
export function useSetDepthEnabled() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (enabled: boolean) => marketMakerApi.setDepthEnabled(enabled),
onSuccess: (data) => {
toast({ title: '成功', description: data.message });
queryClient.invalidateQueries({ queryKey: ['trading', 'depthEnabled'] });
},
onError: (error: any) => {
toast({ title: '错误', description: error.response?.data?.message || '设置失败', variant: 'destructive' });
},
});
}

View File

@ -0,0 +1,2 @@
export * from './api/market-maker.api';
export * from './hooks/use-market-maker';

View File

@ -563,6 +563,29 @@ class _TradingPageState extends ConsumerState<TradingPage> {
],
),
),
const SizedBox(height: 16),
// ()
if (_selectedTab == 1)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'交易手续费',
style: TextStyle(fontSize: 12, color: _grayText),
),
Text(
'10% 进入积分股池',
style: TextStyle(
fontSize: 12,
color: _green,
fontFamily: 'monospace',
),
),
],
),
),
const SizedBox(height: 24),
//
SizedBox(