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(git status:*)",
|
||||
"Bash(xargs cat:*)",
|
||||
"Bash(ssh -o ProxyJump=ceshi@103.39.231.231 ceshi@192.168.1.111 \"docker ps | grep mining\")"
|
||||
"Bash(ssh -o ProxyJump=ceshi@103.39.231.231 ceshi@192.168.1.111 \"docker ps | grep mining\")",
|
||||
"Bash(dir /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\trading-service\\\\src\\\\application\\\\services\")",
|
||||
"Bash(DATABASE_URL=\"postgresql://postgres:password@localhost:5432/trading_db?schema=public\" npx prisma migrate dev:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
// 是否启用买入功能(默认关闭)
|
||||
buyEnabled Boolean @default(false) @map("buy_enabled")
|
||||
// 是否启用深度显示(默认关闭)
|
||||
depthEnabled Boolean @default(false) @map("depth_enabled")
|
||||
// 启动时间
|
||||
activatedAt DateTime? @map("activated_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
|
@ -168,6 +170,9 @@ model Order {
|
|||
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
|
||||
burnMultiplier Decimal @default(0) @map("burn_multiplier") @db.Decimal(30, 18) // 销毁倍数
|
||||
effectiveQuantity Decimal @default(0) @map("effective_quantity") @db.Decimal(30, 8) // 有效卖出量(含销毁)
|
||||
// 订单来源
|
||||
source String @default("USER") // USER, MARKET_MAKER, DEX_BOT, SYSTEM
|
||||
sourceLabel String? @map("source_label") // 可读标签:如 "用户挂单", "做市商吃单"
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
cancelledAt DateTime?
|
||||
|
|
@ -179,6 +184,7 @@ model Order {
|
|||
@@index([accountSequence, status])
|
||||
@@index([type, status, price])
|
||||
@@index([createdAt(sort: Desc)])
|
||||
@@index([source])
|
||||
@@map("orders")
|
||||
}
|
||||
|
||||
|
|
@ -195,12 +201,17 @@ model Trade {
|
|||
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
|
||||
effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量(quantity + burnQuantity)
|
||||
amount Decimal @db.Decimal(30, 8) // effectiveQty * price(卖出交易额)
|
||||
// 交易来源标识
|
||||
buyerSource String @default("USER") @map("buyer_source") // USER, MARKET_MAKER, DEX_BOT, SYSTEM
|
||||
sellerSource String @default("USER") @map("seller_source") // USER, MARKET_MAKER, DEX_BOT, SYSTEM
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
buyOrder Order @relation(fields: [buyOrderId], references: [id])
|
||||
|
||||
@@index([buyerSequence])
|
||||
@@index([sellerSequence])
|
||||
@@index([buyerSource])
|
||||
@@index([sellerSource])
|
||||
@@index([createdAt(sort: Desc)])
|
||||
@@map("trades")
|
||||
}
|
||||
|
|
@ -423,3 +434,152 @@ model ProcessedEvent {
|
|||
@@index([processedAt])
|
||||
@@map("processed_events")
|
||||
}
|
||||
|
||||
// ==================== 做市商 ====================
|
||||
|
||||
// 做市商配置
|
||||
model MarketMakerConfig {
|
||||
id String @id @default(uuid())
|
||||
name String @unique // 做市商名称,如 "MAIN_MARKET_MAKER"
|
||||
accountSequence String @unique @map("account_sequence") // 做市商专用交易账户
|
||||
// 资金配置(从TradingAccount同步,此处仅用于快速查询)
|
||||
cashBalance Decimal @default(0) @map("cash_balance") @db.Decimal(30, 8) // 资金池余额
|
||||
shareBalance Decimal @default(0) @map("share_balance") @db.Decimal(30, 8) // 持有积分股余额
|
||||
frozenCash Decimal @default(0) @map("frozen_cash") @db.Decimal(30, 8) // 冻结资金
|
||||
frozenShares Decimal @default(0) @map("frozen_shares") @db.Decimal(30, 8) // 冻结积分股
|
||||
|
||||
// ============ 吃单策略配置(原有) ============
|
||||
maxBuyRatio Decimal @default(0.05) @map("max_buy_ratio") @db.Decimal(10, 4) // 单次最大买入比例(资金池的百分比)
|
||||
minIntervalMs Int @default(1000) @map("min_interval_ms") // 最小吃单间隔(毫秒)
|
||||
maxIntervalMs Int @default(4000) @map("max_interval_ms") // 最大吃单间隔(毫秒)
|
||||
priceStrategy String @default("TAKER") @map("price_strategy") // 价格策略: TAKER(按卖单价), MARKET(市场价), DISCOUNT(折扣价)
|
||||
discountRate Decimal @default(1.0) @map("discount_rate") @db.Decimal(10, 4) // 折扣率(仅DISCOUNT策略时生效)
|
||||
|
||||
// ============ 双边挂单配置(深度做市) ============
|
||||
// 挂单模式开关
|
||||
makerEnabled Boolean @default(false) @map("maker_enabled") // 是否启用挂单模式(双边深度)
|
||||
|
||||
// 买单挂单配置
|
||||
bidEnabled Boolean @default(true) @map("bid_enabled") // 是否挂买单
|
||||
bidLevels Int @default(5) @map("bid_levels") // 买单档位数
|
||||
bidSpread Decimal @default(0.01) @map("bid_spread") @db.Decimal(10, 4) // 买单价差(相对市价,如0.01表示1%)
|
||||
bidLevelSpacing Decimal @default(0.005) @map("bid_level_spacing") @db.Decimal(10, 4) // 买单档位间距(如0.005表示0.5%)
|
||||
bidQuantityPerLevel Decimal @default(1000) @map("bid_quantity_per_level") @db.Decimal(30, 8) // 每档买单数量
|
||||
|
||||
// 卖单挂单配置
|
||||
askEnabled Boolean @default(true) @map("ask_enabled") // 是否挂卖单
|
||||
askLevels Int @default(5) @map("ask_levels") // 卖单档位数
|
||||
askSpread Decimal @default(0.01) @map("ask_spread") @db.Decimal(10, 4) // 卖单价差(相对市价)
|
||||
askLevelSpacing Decimal @default(0.005) @map("ask_level_spacing") @db.Decimal(10, 4) // 卖单档位间距
|
||||
askQuantityPerLevel Decimal @default(1000) @map("ask_quantity_per_level") @db.Decimal(30, 8) // 每档卖单数量
|
||||
|
||||
// 挂单刷新配置
|
||||
refreshIntervalMs Int @default(60000) @map("refresh_interval_ms") // 挂单刷新间隔(毫秒)
|
||||
lastRefreshAt DateTime? @map("last_refresh_at") // 上次刷新时间
|
||||
|
||||
// ============ 运行状态 ============
|
||||
isActive Boolean @default(false) @map("is_active") // 是否启用(吃单模式)
|
||||
lastRunAt DateTime? @map("last_run_at") // 上次运行时间
|
||||
|
||||
// ============ 统计 ============
|
||||
totalBuyCount Int @default(0) @map("total_buy_count") // 累计买入次数
|
||||
totalBuyQuantity Decimal @default(0) @map("total_buy_quantity") @db.Decimal(30, 8) // 累计买入量
|
||||
totalBuyAmount Decimal @default(0) @map("total_buy_amount") @db.Decimal(30, 8) // 累计买入金额
|
||||
totalSellCount Int @default(0) @map("total_sell_count") // 累计卖出次数
|
||||
totalSellQuantity Decimal @default(0) @map("total_sell_quantity") @db.Decimal(30, 8) // 累计卖出量
|
||||
totalSellAmount Decimal @default(0) @map("total_sell_amount") @db.Decimal(30, 8) // 累计卖出金额
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
ledgers MarketMakerLedger[]
|
||||
makerOrders MarketMakerOrder[]
|
||||
|
||||
@@map("market_maker_configs")
|
||||
}
|
||||
|
||||
// 做市商挂单记录(跟踪做市商当前活跃的挂单)
|
||||
model MarketMakerOrder {
|
||||
id String @id @default(uuid())
|
||||
marketMakerId String @map("market_maker_id")
|
||||
orderId String @unique @map("order_id") // 关联的订单ID
|
||||
orderNo String @unique @map("order_no") // 关联的订单号
|
||||
side String // BID(买单), ASK(卖单)
|
||||
level Int // 档位(1=最优,越大越远离市价)
|
||||
price Decimal @db.Decimal(30, 18) // 挂单价格
|
||||
quantity Decimal @db.Decimal(30, 8) // 挂单数量
|
||||
remainingQty Decimal @map("remaining_qty") @db.Decimal(30, 8) // 剩余数量
|
||||
status String @default("ACTIVE") // ACTIVE, FILLED, CANCELLED
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
marketMaker MarketMakerConfig @relation(fields: [marketMakerId], references: [id])
|
||||
|
||||
@@index([marketMakerId, side, status])
|
||||
@@index([status])
|
||||
@@map("market_maker_orders")
|
||||
}
|
||||
|
||||
// 做市商分类账(流水明细)
|
||||
model MarketMakerLedger {
|
||||
id String @id @default(uuid())
|
||||
marketMakerId String @map("market_maker_id")
|
||||
// 交易类型
|
||||
type String // DEPOSIT(充值), WITHDRAW(提现), BUY(买入), SELL(卖出), FREEZE(冻结), UNFREEZE(解冻)
|
||||
assetType String @map("asset_type") // CASH, SHARE
|
||||
// 金额变动
|
||||
amount Decimal @db.Decimal(30, 8)
|
||||
balanceBefore Decimal @map("balance_before") @db.Decimal(30, 8)
|
||||
balanceAfter Decimal @map("balance_after") @db.Decimal(30, 8)
|
||||
// 关联信息
|
||||
tradeId String? @map("trade_id") // 关联成交ID
|
||||
tradeNo String? @map("trade_no") // 关联成交号
|
||||
orderId String? @map("order_id") // 关联订单ID
|
||||
orderNo String? @map("order_no") // 关联订单号
|
||||
// 交易对手方
|
||||
counterpartySeq String? @map("counterparty_seq") // 交易对手账户序列号
|
||||
counterpartyId String? @map("counterparty_id") // 交易对手用户ID
|
||||
// 价格信息(仅BUY/SELL时)
|
||||
price Decimal? @db.Decimal(30, 18) // 成交价格
|
||||
quantity Decimal? @db.Decimal(30, 8) // 成交数量
|
||||
// 备注
|
||||
memo String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
marketMaker MarketMakerConfig @relation(fields: [marketMakerId], references: [id])
|
||||
|
||||
@@index([marketMakerId, createdAt(sort: Desc)])
|
||||
@@index([type])
|
||||
@@index([tradeNo])
|
||||
@@index([orderNo])
|
||||
@@index([counterpartySeq])
|
||||
@@index([createdAt(sort: Desc)])
|
||||
@@map("market_maker_ledgers")
|
||||
}
|
||||
|
||||
// 做市商日统计
|
||||
model MarketMakerDailyStats {
|
||||
id String @id @default(uuid())
|
||||
marketMakerId String @map("market_maker_id")
|
||||
date DateTime @db.Date
|
||||
// 买入统计
|
||||
buyCount Int @default(0) @map("buy_count")
|
||||
buyQuantity Decimal @default(0) @map("buy_quantity") @db.Decimal(30, 8)
|
||||
buyAmount Decimal @default(0) @map("buy_amount") @db.Decimal(30, 8)
|
||||
avgBuyPrice Decimal @default(0) @map("avg_buy_price") @db.Decimal(30, 18)
|
||||
// 卖出统计
|
||||
sellCount Int @default(0) @map("sell_count")
|
||||
sellQuantity Decimal @default(0) @map("sell_quantity") @db.Decimal(30, 8)
|
||||
sellAmount Decimal @default(0) @map("sell_amount") @db.Decimal(30, 8)
|
||||
avgSellPrice Decimal @default(0) @map("avg_sell_price") @db.Decimal(30, 18)
|
||||
// 盈亏
|
||||
realizedPnl Decimal @default(0) @map("realized_pnl") @db.Decimal(30, 8) // 已实现盈亏
|
||||
// 余额快照
|
||||
cashBalanceEnd Decimal @default(0) @map("cash_balance_end") @db.Decimal(30, 8)
|
||||
shareBalanceEnd Decimal @default(0) @map("share_balance_end") @db.Decimal(30, 8)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([marketMakerId, date])
|
||||
@@index([marketMakerId, date(sort: Desc)])
|
||||
@@map("market_maker_daily_stats")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { AdminController } from './controllers/admin.controller';
|
|||
import { PriceController } from './controllers/price.controller';
|
||||
import { BurnController } from './controllers/burn.controller';
|
||||
import { AssetController } from './controllers/asset.controller';
|
||||
import { MarketMakerController } from './controllers/market-maker.controller';
|
||||
|
||||
@Module({
|
||||
imports: [ApplicationModule, InfrastructureModule],
|
||||
|
|
@ -19,6 +20,7 @@ import { AssetController } from './controllers/asset.controller';
|
|||
PriceController,
|
||||
BurnController,
|
||||
AssetController,
|
||||
MarketMakerController,
|
||||
],
|
||||
})
|
||||
export class ApiModule {}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ class SetBuyEnabledDto {
|
|||
enabled: boolean;
|
||||
}
|
||||
|
||||
class SetDepthEnabledDto {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@ApiTags('Admin')
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
|
|
@ -165,4 +169,29 @@ export class AdminController {
|
|||
message: '交易系统已关闭,每分钟销毁已暂停',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('trading/depth-enabled')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '获取深度显示开关状态' })
|
||||
@ApiResponse({ status: 200, description: '返回深度显示是否启用' })
|
||||
async getDepthEnabled() {
|
||||
const config = await this.tradingConfigRepository.getConfig();
|
||||
return {
|
||||
enabled: config?.depthEnabled ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('trading/depth-enabled')
|
||||
@Public() // TODO: 生产环境应添加管理员权限验证
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '设置深度显示开关' })
|
||||
@ApiResponse({ status: 200, description: '深度显示开关设置成功' })
|
||||
async setDepthEnabled(@Body() dto: SetDepthEnabledDto) {
|
||||
await this.tradingConfigRepository.setDepthEnabled(dto.enabled);
|
||||
return {
|
||||
success: true,
|
||||
enabled: dto.enabled,
|
||||
message: dto.enabled ? '深度显示已开启' : '深度显示已关闭',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { PriceService } from '../../application/services/price.service';
|
||||
import { MarketMakerService } from '../../application/services/market-maker.service';
|
||||
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
|
||||
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('Price')
|
||||
@Controller('price')
|
||||
export class PriceController {
|
||||
constructor(private readonly priceService: PriceService) {}
|
||||
constructor(
|
||||
private readonly priceService: PriceService,
|
||||
private readonly marketMakerService: MarketMakerService,
|
||||
private readonly tradingConfigRepository: TradingConfigRepository,
|
||||
) {}
|
||||
|
||||
@Get('current')
|
||||
@Public()
|
||||
|
|
@ -61,4 +67,29 @@ export class PriceController {
|
|||
) {
|
||||
return this.priceService.getKlines(period, Math.min(limit, 500));
|
||||
}
|
||||
|
||||
@Get('depth')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '获取订单簿深度(买卖盘深度)' })
|
||||
@ApiQuery({ name: 'levels', required: false, type: Number, description: '深度档位数(默认10)' })
|
||||
async getDepth(@Query('levels') levels?: string) {
|
||||
// 检查深度功能是否启用
|
||||
const config = await this.tradingConfigRepository.getConfig();
|
||||
if (!config || !config.depthEnabled) {
|
||||
return {
|
||||
success: true,
|
||||
bids: [],
|
||||
asks: [],
|
||||
timestamp: Date.now(),
|
||||
message: '深度功能未启用',
|
||||
};
|
||||
}
|
||||
|
||||
const depth = await this.marketMakerService.getDepth(levels ? parseInt(levels, 10) : 10);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...depth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { TransferService } from './services/transfer.service';
|
|||
import { PriceService } from './services/price.service';
|
||||
import { BurnService } from './services/burn.service';
|
||||
import { AssetService } from './services/asset.service';
|
||||
import { MarketMakerService } from './services/market-maker.service';
|
||||
import { OutboxScheduler } from './schedulers/outbox.scheduler';
|
||||
import { BurnScheduler } from './schedulers/burn.scheduler';
|
||||
|
||||
|
|
@ -18,10 +19,11 @@ import { BurnScheduler } from './schedulers/burn.scheduler';
|
|||
AssetService,
|
||||
OrderService,
|
||||
TransferService,
|
||||
MarketMakerService,
|
||||
// Schedulers
|
||||
OutboxScheduler,
|
||||
BurnScheduler,
|
||||
],
|
||||
exports: [OrderService, TransferService, PriceService, BurnService, AssetService],
|
||||
exports: [OrderService, TransferService, PriceService, BurnService, AssetService, MarketMakerService],
|
||||
})
|
||||
export class ApplicationModule {}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,7 @@ import { CirculationPoolRepository } from '../../infrastructure/persistence/repo
|
|||
import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository';
|
||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||
import { RedisService } from '../../infrastructure/redis/redis.service';
|
||||
import { OrderAggregate, OrderType, OrderStatus } from '../../domain/aggregates/order.aggregate';
|
||||
import { OrderAggregate, OrderType, OrderStatus, OrderSource } from '../../domain/aggregates/order.aggregate';
|
||||
import { TradingAccountAggregate } from '../../domain/aggregates/trading-account.aggregate';
|
||||
import { MatchingEngineService } from '../../domain/services/matching-engine.service';
|
||||
import { Money } from '../../domain/value-objects/money.vo';
|
||||
|
|
@ -40,6 +40,8 @@ export class OrderService {
|
|||
type: OrderType,
|
||||
price: string,
|
||||
quantity: string,
|
||||
source: OrderSource = OrderSource.USER,
|
||||
sourceLabel?: string,
|
||||
): Promise<{ orderId: string; orderNo: string; status: OrderStatus; filledQuantity: string }> {
|
||||
const lockValue = await this.redis.acquireLock(`order:create:${accountSequence}`, 10);
|
||||
if (!lockValue) {
|
||||
|
|
@ -72,7 +74,7 @@ export class OrderService {
|
|||
const orderNo = this.generateOrderNo();
|
||||
|
||||
// 创建订单
|
||||
const order = OrderAggregate.create(orderNo, accountSequence, type, priceAmount, quantityAmount);
|
||||
const order = OrderAggregate.create(orderNo, accountSequence, type, priceAmount, quantityAmount, source, sourceLabel);
|
||||
|
||||
// 冻结资产
|
||||
if (type === OrderType.BUY) {
|
||||
|
|
@ -176,7 +178,7 @@ export class OrderService {
|
|||
// 计算交易额 = 有效数量 × 价格
|
||||
const tradeAmount = new Money(effectiveQuantity.value.times(match.trade.price.value));
|
||||
|
||||
// 保存成交记录(包含销毁信息)
|
||||
// 保存成交记录(包含销毁信息和来源标识)
|
||||
await this.prisma.trade.create({
|
||||
data: {
|
||||
tradeNo: match.trade.tradeNo,
|
||||
|
|
@ -189,6 +191,8 @@ export class OrderService {
|
|||
burnQuantity: burnQuantity.value,
|
||||
effectiveQty: effectiveQuantity.value,
|
||||
amount: tradeAmount.value,
|
||||
buyerSource: match.buyOrder.source,
|
||||
sellerSource: match.sellOrder.source,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ export enum OrderStatus {
|
|||
CANCELLED = 'CANCELLED',
|
||||
}
|
||||
|
||||
export enum OrderSource {
|
||||
USER = 'USER',
|
||||
MARKET_MAKER = 'MARKET_MAKER',
|
||||
DEX_BOT = 'DEX_BOT',
|
||||
SYSTEM = 'SYSTEM',
|
||||
}
|
||||
|
||||
export interface TradeInfo {
|
||||
tradeNo: string;
|
||||
counterpartyOrderId: string;
|
||||
|
|
@ -29,6 +36,8 @@ export class OrderAggregate {
|
|||
private _accountSequence: string;
|
||||
private _type: OrderType;
|
||||
private _status: OrderStatus;
|
||||
private _source: OrderSource;
|
||||
private _sourceLabel: string | null;
|
||||
private _price: Money;
|
||||
private _quantity: Money;
|
||||
private _filledQuantity: Money;
|
||||
|
|
@ -46,6 +55,8 @@ export class OrderAggregate {
|
|||
type: OrderType,
|
||||
price: Money,
|
||||
quantity: Money,
|
||||
source: OrderSource = OrderSource.USER,
|
||||
sourceLabel: string | null = null,
|
||||
id: string | null = null,
|
||||
) {
|
||||
this._id = id;
|
||||
|
|
@ -53,6 +64,8 @@ export class OrderAggregate {
|
|||
this._accountSequence = accountSequence;
|
||||
this._type = type;
|
||||
this._status = OrderStatus.PENDING;
|
||||
this._source = source;
|
||||
this._sourceLabel = sourceLabel;
|
||||
this._price = price;
|
||||
this._quantity = quantity;
|
||||
this._filledQuantity = Money.zero();
|
||||
|
|
@ -68,6 +81,8 @@ export class OrderAggregate {
|
|||
type: OrderType,
|
||||
price: Money,
|
||||
quantity: Money,
|
||||
source: OrderSource = OrderSource.USER,
|
||||
sourceLabel: string | null = null,
|
||||
): OrderAggregate {
|
||||
if (price.isZero()) {
|
||||
throw new Error('Price cannot be zero');
|
||||
|
|
@ -75,7 +90,7 @@ export class OrderAggregate {
|
|||
if (quantity.isZero()) {
|
||||
throw new Error('Quantity cannot be zero');
|
||||
}
|
||||
return new OrderAggregate(orderNo, accountSequence, type, price, quantity);
|
||||
return new OrderAggregate(orderNo, accountSequence, type, price, quantity, source, sourceLabel);
|
||||
}
|
||||
|
||||
static reconstitute(props: {
|
||||
|
|
@ -84,6 +99,8 @@ export class OrderAggregate {
|
|||
accountSequence: string;
|
||||
type: OrderType;
|
||||
status: OrderStatus;
|
||||
source: OrderSource;
|
||||
sourceLabel: string | null;
|
||||
price: Money;
|
||||
quantity: Money;
|
||||
filledQuantity: Money;
|
||||
|
|
@ -100,6 +117,8 @@ export class OrderAggregate {
|
|||
props.type,
|
||||
props.price,
|
||||
props.quantity,
|
||||
props.source,
|
||||
props.sourceLabel,
|
||||
props.id,
|
||||
);
|
||||
order._status = props.status;
|
||||
|
|
@ -119,6 +138,8 @@ export class OrderAggregate {
|
|||
get accountSequence(): string { return this._accountSequence; }
|
||||
get type(): OrderType { return this._type; }
|
||||
get status(): OrderStatus { return this._status; }
|
||||
get source(): OrderSource { return this._source; }
|
||||
get sourceLabel(): string | null { return this._sourceLabel; }
|
||||
get price(): Money { return this._price; }
|
||||
get quantity(): Money { return this._quantity; }
|
||||
get filledQuantity(): Money { return this._filledQuantity; }
|
||||
|
|
|
|||
|
|
@ -288,6 +288,63 @@ export class TradingAccountAggregate {
|
|||
});
|
||||
}
|
||||
|
||||
// 提现现金
|
||||
withdraw(amount: Money, referenceId: string): void {
|
||||
if (this.availableCash.isLessThan(amount)) {
|
||||
throw new Error('Insufficient available cash for withdrawal');
|
||||
}
|
||||
const balanceBefore = this._cashBalance;
|
||||
this._cashBalance = this._cashBalance.subtract(amount);
|
||||
this._pendingTransactions.push({
|
||||
type: TradingTransactionType.WITHDRAW,
|
||||
assetType: AssetType.CASH,
|
||||
amount,
|
||||
balanceBefore,
|
||||
balanceAfter: this._cashBalance,
|
||||
referenceId,
|
||||
referenceType: 'WITHDRAW',
|
||||
description: '提现',
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// 充值积分股(做市商用)
|
||||
depositShares(amount: Money, referenceId: string): void {
|
||||
const balanceBefore = this._shareBalance;
|
||||
this._shareBalance = this._shareBalance.add(amount);
|
||||
this._pendingTransactions.push({
|
||||
type: TradingTransactionType.DEPOSIT,
|
||||
assetType: AssetType.SHARE,
|
||||
amount,
|
||||
balanceBefore,
|
||||
balanceAfter: this._shareBalance,
|
||||
referenceId,
|
||||
referenceType: 'DEPOSIT',
|
||||
description: '积分股充值',
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// 提取积分股(做市商用)
|
||||
withdrawShares(amount: Money, referenceId: string): void {
|
||||
if (this.availableShares.isLessThan(amount)) {
|
||||
throw new Error('Insufficient available shares for withdrawal');
|
||||
}
|
||||
const balanceBefore = this._shareBalance;
|
||||
this._shareBalance = this._shareBalance.subtract(amount);
|
||||
this._pendingTransactions.push({
|
||||
type: TradingTransactionType.WITHDRAW,
|
||||
assetType: AssetType.SHARE,
|
||||
amount,
|
||||
balanceBefore,
|
||||
balanceAfter: this._shareBalance,
|
||||
referenceId,
|
||||
referenceType: 'WITHDRAW',
|
||||
description: '积分股提取',
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
clearPendingTransactions(): void {
|
||||
this._pendingTransactions = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { OrderAggregate, OrderType, OrderStatus } from '../../../domain/aggregates/order.aggregate';
|
||||
import { OrderAggregate, OrderType, OrderStatus, OrderSource } from '../../../domain/aggregates/order.aggregate';
|
||||
import { Money } from '../../../domain/value-objects/money.vo';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -25,6 +25,8 @@ export class OrderRepository {
|
|||
accountSequence: aggregate.accountSequence,
|
||||
type: aggregate.type,
|
||||
status: aggregate.status,
|
||||
source: aggregate.source,
|
||||
sourceLabel: aggregate.sourceLabel,
|
||||
price: aggregate.price.value,
|
||||
quantity: aggregate.quantity.value,
|
||||
filledQuantity: aggregate.filledQuantity.value,
|
||||
|
|
@ -60,6 +62,8 @@ export class OrderRepository {
|
|||
accountSequence: aggregate.accountSequence,
|
||||
type: aggregate.type,
|
||||
status: aggregate.status,
|
||||
source: aggregate.source,
|
||||
sourceLabel: aggregate.sourceLabel,
|
||||
price: aggregate.price.value,
|
||||
quantity: aggregate.quantity.value,
|
||||
filledQuantity: aggregate.filledQuantity.value,
|
||||
|
|
@ -164,6 +168,51 @@ export class OrderRepository {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找最老的待成交卖单(用于做市商吃单)
|
||||
*/
|
||||
async findOldestPendingSellOrder(): Promise<OrderAggregate | null> {
|
||||
const record = await this.prisma.order.findFirst({
|
||||
where: {
|
||||
type: OrderType.SELL,
|
||||
status: { in: [OrderStatus.PENDING, OrderStatus.PARTIAL] },
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
if (!record) return null;
|
||||
return this.toDomain(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按来源查询订单
|
||||
*/
|
||||
async findBySource(
|
||||
source: OrderSource,
|
||||
options?: { type?: OrderType; status?: OrderStatus; page?: number; pageSize?: number },
|
||||
): Promise<{ data: OrderAggregate[]; total: number }> {
|
||||
const where: any = { source };
|
||||
if (options?.type) where.type = options.type;
|
||||
if (options?.status) where.status = options.status;
|
||||
|
||||
const page = options?.page ?? 1;
|
||||
const pageSize = options?.pageSize ?? 50;
|
||||
|
||||
const [records, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: records.map((r) => this.toDomain(r)),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
private toDomain(record: any): OrderAggregate {
|
||||
return OrderAggregate.reconstitute({
|
||||
id: record.id,
|
||||
|
|
@ -171,6 +220,8 @@ export class OrderRepository {
|
|||
accountSequence: record.accountSequence,
|
||||
type: record.type as OrderType,
|
||||
status: record.status as OrderStatus,
|
||||
source: (record.source as OrderSource) || OrderSource.USER,
|
||||
sourceLabel: record.sourceLabel,
|
||||
price: new Money(record.price),
|
||||
quantity: new Money(record.quantity),
|
||||
filledQuantity: new Money(record.filledQuantity),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface TradingConfigEntity {
|
|||
minuteBurnRate: Money;
|
||||
isActive: boolean;
|
||||
buyEnabled: boolean;
|
||||
depthEnabled: boolean;
|
||||
activatedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
|
@ -98,6 +99,18 @@ export class TradingConfigRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async setDepthEnabled(enabled: boolean): Promise<void> {
|
||||
const config = await this.prisma.tradingConfig.findFirst();
|
||||
if (!config) {
|
||||
throw new Error('Trading config not initialized');
|
||||
}
|
||||
|
||||
await this.prisma.tradingConfig.update({
|
||||
where: { id: config.id },
|
||||
data: { depthEnabled: enabled },
|
||||
});
|
||||
}
|
||||
|
||||
private toDomain(record: any): TradingConfigEntity {
|
||||
return {
|
||||
id: record.id,
|
||||
|
|
@ -107,6 +120,7 @@ export class TradingConfigRepository {
|
|||
minuteBurnRate: new Money(record.minuteBurnRate),
|
||||
isActive: record.isActive,
|
||||
buyEnabled: record.buyEnabled ?? false,
|
||||
depthEnabled: record.depthEnabled ?? false,
|
||||
activatedAt: record.activatedAt,
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
ChevronRight,
|
||||
ArrowLeftRight,
|
||||
Bot,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ const menuItems = [
|
|||
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: '用户管理', href: '/users', icon: Users },
|
||||
{ name: '交易管理', href: '/trading', icon: ArrowLeftRight },
|
||||
{ name: '做市商管理', href: '/market-maker', icon: Bot },
|
||||
{ name: '配置管理', href: '/configs', icon: Settings },
|
||||
{ name: '系统账户', href: '/system-accounts', icon: Building2 },
|
||||
{ name: '报表统计', href: '/reports', icon: FileBarChart },
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
// 提交按钮
|
||||
SizedBox(
|
||||
|
|
|
|||
Loading…
Reference in New Issue