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:
parent
416495a398
commit
3b6bd29283
|
|
@ -782,7 +782,9 @@
|
||||||
"Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/mining_db?schema=public\" npx prisma migrate diff:*)",
|
"Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/mining_db?schema=public\" npx prisma migrate diff:*)",
|
||||||
"Bash(git status:*)",
|
"Bash(git status:*)",
|
||||||
"Bash(xargs cat:*)",
|
"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": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -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 $$;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -24,6 +24,8 @@ model TradingConfig {
|
||||||
isActive Boolean @default(false) @map("is_active")
|
isActive Boolean @default(false) @map("is_active")
|
||||||
// 是否启用买入功能(默认关闭)
|
// 是否启用买入功能(默认关闭)
|
||||||
buyEnabled Boolean @default(false) @map("buy_enabled")
|
buyEnabled Boolean @default(false) @map("buy_enabled")
|
||||||
|
// 是否启用深度显示(默认关闭)
|
||||||
|
depthEnabled Boolean @default(false) @map("depth_enabled")
|
||||||
// 启动时间
|
// 启动时间
|
||||||
activatedAt DateTime? @map("activated_at")
|
activatedAt DateTime? @map("activated_at")
|
||||||
createdAt DateTime @default(now()) @map("created_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) // 卖出销毁量
|
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
|
||||||
burnMultiplier Decimal @default(0) @map("burn_multiplier") @db.Decimal(30, 18) // 销毁倍数
|
burnMultiplier Decimal @default(0) @map("burn_multiplier") @db.Decimal(30, 18) // 销毁倍数
|
||||||
effectiveQuantity Decimal @default(0) @map("effective_quantity") @db.Decimal(30, 8) // 有效卖出量(含销毁)
|
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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
cancelledAt DateTime?
|
cancelledAt DateTime?
|
||||||
|
|
@ -179,6 +184,7 @@ model Order {
|
||||||
@@index([accountSequence, status])
|
@@index([accountSequence, status])
|
||||||
@@index([type, status, price])
|
@@index([type, status, price])
|
||||||
@@index([createdAt(sort: Desc)])
|
@@index([createdAt(sort: Desc)])
|
||||||
|
@@index([source])
|
||||||
@@map("orders")
|
@@map("orders")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,12 +201,17 @@ model Trade {
|
||||||
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
|
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
|
||||||
effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量(quantity + burnQuantity)
|
effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量(quantity + burnQuantity)
|
||||||
amount Decimal @db.Decimal(30, 8) // effectiveQty * price(卖出交易额)
|
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")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
buyOrder Order @relation(fields: [buyOrderId], references: [id])
|
buyOrder Order @relation(fields: [buyOrderId], references: [id])
|
||||||
|
|
||||||
@@index([buyerSequence])
|
@@index([buyerSequence])
|
||||||
@@index([sellerSequence])
|
@@index([sellerSequence])
|
||||||
|
@@index([buyerSource])
|
||||||
|
@@index([sellerSource])
|
||||||
@@index([createdAt(sort: Desc)])
|
@@index([createdAt(sort: Desc)])
|
||||||
@@map("trades")
|
@@map("trades")
|
||||||
}
|
}
|
||||||
|
|
@ -423,3 +434,152 @@ model ProcessedEvent {
|
||||||
@@index([processedAt])
|
@@index([processedAt])
|
||||||
@@map("processed_events")
|
@@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")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { AdminController } from './controllers/admin.controller';
|
||||||
import { PriceController } from './controllers/price.controller';
|
import { PriceController } from './controllers/price.controller';
|
||||||
import { BurnController } from './controllers/burn.controller';
|
import { BurnController } from './controllers/burn.controller';
|
||||||
import { AssetController } from './controllers/asset.controller';
|
import { AssetController } from './controllers/asset.controller';
|
||||||
|
import { MarketMakerController } from './controllers/market-maker.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ApplicationModule, InfrastructureModule],
|
imports: [ApplicationModule, InfrastructureModule],
|
||||||
|
|
@ -19,6 +20,7 @@ import { AssetController } from './controllers/asset.controller';
|
||||||
PriceController,
|
PriceController,
|
||||||
BurnController,
|
BurnController,
|
||||||
AssetController,
|
AssetController,
|
||||||
|
MarketMakerController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApiModule {}
|
export class ApiModule {}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ class SetBuyEnabledDto {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SetDepthEnabledDto {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@ApiTags('Admin')
|
@ApiTags('Admin')
|
||||||
@Controller('admin')
|
@Controller('admin')
|
||||||
export class AdminController {
|
export class AdminController {
|
||||||
|
|
@ -165,4 +169,29 @@ export class AdminController {
|
||||||
message: '交易系统已关闭,每分钟销毁已暂停',
|
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 ? '深度显示已开启' : '深度显示已关闭',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { PriceService } from '../../application/services/price.service';
|
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';
|
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||||
|
|
||||||
@ApiTags('Price')
|
@ApiTags('Price')
|
||||||
@Controller('price')
|
@Controller('price')
|
||||||
export class PriceController {
|
export class PriceController {
|
||||||
constructor(private readonly priceService: PriceService) {}
|
constructor(
|
||||||
|
private readonly priceService: PriceService,
|
||||||
|
private readonly marketMakerService: MarketMakerService,
|
||||||
|
private readonly tradingConfigRepository: TradingConfigRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get('current')
|
@Get('current')
|
||||||
@Public()
|
@Public()
|
||||||
|
|
@ -61,4 +67,29 @@ export class PriceController {
|
||||||
) {
|
) {
|
||||||
return this.priceService.getKlines(period, Math.min(limit, 500));
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { TransferService } from './services/transfer.service';
|
||||||
import { PriceService } from './services/price.service';
|
import { PriceService } from './services/price.service';
|
||||||
import { BurnService } from './services/burn.service';
|
import { BurnService } from './services/burn.service';
|
||||||
import { AssetService } from './services/asset.service';
|
import { AssetService } from './services/asset.service';
|
||||||
|
import { MarketMakerService } from './services/market-maker.service';
|
||||||
import { OutboxScheduler } from './schedulers/outbox.scheduler';
|
import { OutboxScheduler } from './schedulers/outbox.scheduler';
|
||||||
import { BurnScheduler } from './schedulers/burn.scheduler';
|
import { BurnScheduler } from './schedulers/burn.scheduler';
|
||||||
|
|
||||||
|
|
@ -18,10 +19,11 @@ import { BurnScheduler } from './schedulers/burn.scheduler';
|
||||||
AssetService,
|
AssetService,
|
||||||
OrderService,
|
OrderService,
|
||||||
TransferService,
|
TransferService,
|
||||||
|
MarketMakerService,
|
||||||
// Schedulers
|
// Schedulers
|
||||||
OutboxScheduler,
|
OutboxScheduler,
|
||||||
BurnScheduler,
|
BurnScheduler,
|
||||||
],
|
],
|
||||||
exports: [OrderService, TransferService, PriceService, BurnService, AssetService],
|
exports: [OrderService, TransferService, PriceService, BurnService, AssetService, MarketMakerService],
|
||||||
})
|
})
|
||||||
export class ApplicationModule {}
|
export class ApplicationModule {}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,7 @@ import { CirculationPoolRepository } from '../../infrastructure/persistence/repo
|
||||||
import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository';
|
import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository';
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
import { RedisService } from '../../infrastructure/redis/redis.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 { TradingAccountAggregate } from '../../domain/aggregates/trading-account.aggregate';
|
||||||
import { MatchingEngineService } from '../../domain/services/matching-engine.service';
|
import { MatchingEngineService } from '../../domain/services/matching-engine.service';
|
||||||
import { Money } from '../../domain/value-objects/money.vo';
|
import { Money } from '../../domain/value-objects/money.vo';
|
||||||
|
|
@ -40,6 +40,8 @@ export class OrderService {
|
||||||
type: OrderType,
|
type: OrderType,
|
||||||
price: string,
|
price: string,
|
||||||
quantity: string,
|
quantity: string,
|
||||||
|
source: OrderSource = OrderSource.USER,
|
||||||
|
sourceLabel?: string,
|
||||||
): Promise<{ orderId: string; orderNo: string; status: OrderStatus; filledQuantity: string }> {
|
): Promise<{ orderId: string; orderNo: string; status: OrderStatus; filledQuantity: string }> {
|
||||||
const lockValue = await this.redis.acquireLock(`order:create:${accountSequence}`, 10);
|
const lockValue = await this.redis.acquireLock(`order:create:${accountSequence}`, 10);
|
||||||
if (!lockValue) {
|
if (!lockValue) {
|
||||||
|
|
@ -72,7 +74,7 @@ export class OrderService {
|
||||||
const orderNo = this.generateOrderNo();
|
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) {
|
if (type === OrderType.BUY) {
|
||||||
|
|
@ -176,7 +178,7 @@ export class OrderService {
|
||||||
// 计算交易额 = 有效数量 × 价格
|
// 计算交易额 = 有效数量 × 价格
|
||||||
const tradeAmount = new Money(effectiveQuantity.value.times(match.trade.price.value));
|
const tradeAmount = new Money(effectiveQuantity.value.times(match.trade.price.value));
|
||||||
|
|
||||||
// 保存成交记录(包含销毁信息)
|
// 保存成交记录(包含销毁信息和来源标识)
|
||||||
await this.prisma.trade.create({
|
await this.prisma.trade.create({
|
||||||
data: {
|
data: {
|
||||||
tradeNo: match.trade.tradeNo,
|
tradeNo: match.trade.tradeNo,
|
||||||
|
|
@ -189,6 +191,8 @@ export class OrderService {
|
||||||
burnQuantity: burnQuantity.value,
|
burnQuantity: burnQuantity.value,
|
||||||
effectiveQty: effectiveQuantity.value,
|
effectiveQty: effectiveQuantity.value,
|
||||||
amount: tradeAmount.value,
|
amount: tradeAmount.value,
|
||||||
|
buyerSource: match.buyOrder.source,
|
||||||
|
sellerSource: match.sellOrder.source,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@ export enum OrderStatus {
|
||||||
CANCELLED = 'CANCELLED',
|
CANCELLED = 'CANCELLED',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrderSource {
|
||||||
|
USER = 'USER',
|
||||||
|
MARKET_MAKER = 'MARKET_MAKER',
|
||||||
|
DEX_BOT = 'DEX_BOT',
|
||||||
|
SYSTEM = 'SYSTEM',
|
||||||
|
}
|
||||||
|
|
||||||
export interface TradeInfo {
|
export interface TradeInfo {
|
||||||
tradeNo: string;
|
tradeNo: string;
|
||||||
counterpartyOrderId: string;
|
counterpartyOrderId: string;
|
||||||
|
|
@ -29,6 +36,8 @@ export class OrderAggregate {
|
||||||
private _accountSequence: string;
|
private _accountSequence: string;
|
||||||
private _type: OrderType;
|
private _type: OrderType;
|
||||||
private _status: OrderStatus;
|
private _status: OrderStatus;
|
||||||
|
private _source: OrderSource;
|
||||||
|
private _sourceLabel: string | null;
|
||||||
private _price: Money;
|
private _price: Money;
|
||||||
private _quantity: Money;
|
private _quantity: Money;
|
||||||
private _filledQuantity: Money;
|
private _filledQuantity: Money;
|
||||||
|
|
@ -46,6 +55,8 @@ export class OrderAggregate {
|
||||||
type: OrderType,
|
type: OrderType,
|
||||||
price: Money,
|
price: Money,
|
||||||
quantity: Money,
|
quantity: Money,
|
||||||
|
source: OrderSource = OrderSource.USER,
|
||||||
|
sourceLabel: string | null = null,
|
||||||
id: string | null = null,
|
id: string | null = null,
|
||||||
) {
|
) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
|
|
@ -53,6 +64,8 @@ export class OrderAggregate {
|
||||||
this._accountSequence = accountSequence;
|
this._accountSequence = accountSequence;
|
||||||
this._type = type;
|
this._type = type;
|
||||||
this._status = OrderStatus.PENDING;
|
this._status = OrderStatus.PENDING;
|
||||||
|
this._source = source;
|
||||||
|
this._sourceLabel = sourceLabel;
|
||||||
this._price = price;
|
this._price = price;
|
||||||
this._quantity = quantity;
|
this._quantity = quantity;
|
||||||
this._filledQuantity = Money.zero();
|
this._filledQuantity = Money.zero();
|
||||||
|
|
@ -68,6 +81,8 @@ export class OrderAggregate {
|
||||||
type: OrderType,
|
type: OrderType,
|
||||||
price: Money,
|
price: Money,
|
||||||
quantity: Money,
|
quantity: Money,
|
||||||
|
source: OrderSource = OrderSource.USER,
|
||||||
|
sourceLabel: string | null = null,
|
||||||
): OrderAggregate {
|
): OrderAggregate {
|
||||||
if (price.isZero()) {
|
if (price.isZero()) {
|
||||||
throw new Error('Price cannot be zero');
|
throw new Error('Price cannot be zero');
|
||||||
|
|
@ -75,7 +90,7 @@ export class OrderAggregate {
|
||||||
if (quantity.isZero()) {
|
if (quantity.isZero()) {
|
||||||
throw new Error('Quantity cannot be zero');
|
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: {
|
static reconstitute(props: {
|
||||||
|
|
@ -84,6 +99,8 @@ export class OrderAggregate {
|
||||||
accountSequence: string;
|
accountSequence: string;
|
||||||
type: OrderType;
|
type: OrderType;
|
||||||
status: OrderStatus;
|
status: OrderStatus;
|
||||||
|
source: OrderSource;
|
||||||
|
sourceLabel: string | null;
|
||||||
price: Money;
|
price: Money;
|
||||||
quantity: Money;
|
quantity: Money;
|
||||||
filledQuantity: Money;
|
filledQuantity: Money;
|
||||||
|
|
@ -100,6 +117,8 @@ export class OrderAggregate {
|
||||||
props.type,
|
props.type,
|
||||||
props.price,
|
props.price,
|
||||||
props.quantity,
|
props.quantity,
|
||||||
|
props.source,
|
||||||
|
props.sourceLabel,
|
||||||
props.id,
|
props.id,
|
||||||
);
|
);
|
||||||
order._status = props.status;
|
order._status = props.status;
|
||||||
|
|
@ -119,6 +138,8 @@ export class OrderAggregate {
|
||||||
get accountSequence(): string { return this._accountSequence; }
|
get accountSequence(): string { return this._accountSequence; }
|
||||||
get type(): OrderType { return this._type; }
|
get type(): OrderType { return this._type; }
|
||||||
get status(): OrderStatus { return this._status; }
|
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 price(): Money { return this._price; }
|
||||||
get quantity(): Money { return this._quantity; }
|
get quantity(): Money { return this._quantity; }
|
||||||
get filledQuantity(): Money { return this._filledQuantity; }
|
get filledQuantity(): Money { return this._filledQuantity; }
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
clearPendingTransactions(): void {
|
||||||
this._pendingTransactions = [];
|
this._pendingTransactions = [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
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';
|
import { Money } from '../../../domain/value-objects/money.vo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -25,6 +25,8 @@ export class OrderRepository {
|
||||||
accountSequence: aggregate.accountSequence,
|
accountSequence: aggregate.accountSequence,
|
||||||
type: aggregate.type,
|
type: aggregate.type,
|
||||||
status: aggregate.status,
|
status: aggregate.status,
|
||||||
|
source: aggregate.source,
|
||||||
|
sourceLabel: aggregate.sourceLabel,
|
||||||
price: aggregate.price.value,
|
price: aggregate.price.value,
|
||||||
quantity: aggregate.quantity.value,
|
quantity: aggregate.quantity.value,
|
||||||
filledQuantity: aggregate.filledQuantity.value,
|
filledQuantity: aggregate.filledQuantity.value,
|
||||||
|
|
@ -60,6 +62,8 @@ export class OrderRepository {
|
||||||
accountSequence: aggregate.accountSequence,
|
accountSequence: aggregate.accountSequence,
|
||||||
type: aggregate.type,
|
type: aggregate.type,
|
||||||
status: aggregate.status,
|
status: aggregate.status,
|
||||||
|
source: aggregate.source,
|
||||||
|
sourceLabel: aggregate.sourceLabel,
|
||||||
price: aggregate.price.value,
|
price: aggregate.price.value,
|
||||||
quantity: aggregate.quantity.value,
|
quantity: aggregate.quantity.value,
|
||||||
filledQuantity: aggregate.filledQuantity.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 {
|
private toDomain(record: any): OrderAggregate {
|
||||||
return OrderAggregate.reconstitute({
|
return OrderAggregate.reconstitute({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
|
|
@ -171,6 +220,8 @@ export class OrderRepository {
|
||||||
accountSequence: record.accountSequence,
|
accountSequence: record.accountSequence,
|
||||||
type: record.type as OrderType,
|
type: record.type as OrderType,
|
||||||
status: record.status as OrderStatus,
|
status: record.status as OrderStatus,
|
||||||
|
source: (record.source as OrderSource) || OrderSource.USER,
|
||||||
|
sourceLabel: record.sourceLabel,
|
||||||
price: new Money(record.price),
|
price: new Money(record.price),
|
||||||
quantity: new Money(record.quantity),
|
quantity: new Money(record.quantity),
|
||||||
filledQuantity: new Money(record.filledQuantity),
|
filledQuantity: new Money(record.filledQuantity),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export interface TradingConfigEntity {
|
||||||
minuteBurnRate: Money;
|
minuteBurnRate: Money;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
buyEnabled: boolean;
|
buyEnabled: boolean;
|
||||||
|
depthEnabled: boolean;
|
||||||
activatedAt: Date | null;
|
activatedAt: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: 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 {
|
private toDomain(record: any): TradingConfigEntity {
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
|
|
@ -107,6 +120,7 @@ export class TradingConfigRepository {
|
||||||
minuteBurnRate: new Money(record.minuteBurnRate),
|
minuteBurnRate: new Money(record.minuteBurnRate),
|
||||||
isActive: record.isActive,
|
isActive: record.isActive,
|
||||||
buyEnabled: record.buyEnabled ?? false,
|
buyEnabled: record.buyEnabled ?? false,
|
||||||
|
depthEnabled: record.depthEnabled ?? false,
|
||||||
activatedAt: record.activatedAt,
|
activatedAt: record.activatedAt,
|
||||||
createdAt: record.createdAt,
|
createdAt: record.createdAt,
|
||||||
updatedAt: record.updatedAt,
|
updatedAt: record.updatedAt,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ArrowLeftRight,
|
ArrowLeftRight,
|
||||||
|
Bot,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ const menuItems = [
|
||||||
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
|
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
|
||||||
{ name: '用户管理', href: '/users', icon: Users },
|
{ name: '用户管理', href: '/users', icon: Users },
|
||||||
{ name: '交易管理', href: '/trading', icon: ArrowLeftRight },
|
{ name: '交易管理', href: '/trading', icon: ArrowLeftRight },
|
||||||
|
{ name: '做市商管理', href: '/market-maker', icon: Bot },
|
||||||
{ name: '配置管理', href: '/configs', icon: Settings },
|
{ name: '配置管理', href: '/configs', icon: Settings },
|
||||||
{ name: '系统账户', href: '/system-accounts', icon: Building2 },
|
{ name: '系统账户', href: '/system-accounts', icon: Building2 },
|
||||||
{ name: '报表统计', href: '/reports', icon: FileBarChart },
|
{ name: '报表统计', href: '/reports', icon: FileBarChart },
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './api/market-maker.api';
|
||||||
|
export * from './hooks/use-market-maker';
|
||||||
|
|
@ -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),
|
const SizedBox(height: 24),
|
||||||
// 提交按钮
|
// 提交按钮
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue