From 3b6bd2928347e0b5b86775443e6f365b9734904a Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 17 Jan 2026 21:11:23 -0800 Subject: [PATCH] =?UTF-8?q?feat(trading):=20=E5=AE=9E=E7=8E=B0=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E7=9A=84CEX=E5=81=9A=E5=B8=82=E5=95=86=E5=8F=8C?= =?UTF-8?q?=E8=BE=B9=E6=B7=B1=E5=BA=A6=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 后端 - 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 --- .claude/settings.local.json | 4 +- .../0003_add_market_maker_depth/migration.sql | 75 ++ .../migration.sql | 98 ++ .../trading-service/prisma/schema.prisma | 160 +++ .../trading-service/src/api/api.module.ts | 2 + .../src/api/controllers/admin.controller.ts | 29 + .../controllers/market-maker.controller.ts | 470 +++++++ .../src/api/controllers/price.controller.ts | 33 +- .../src/application/application.module.ts | 4 +- .../services/market-maker.service.ts | 1168 +++++++++++++++++ .../src/application/services/order.service.ts | 10 +- .../src/domain/aggregates/order.aggregate.ts | 23 +- .../aggregates/trading-account.aggregate.ts | 57 + .../repositories/order.repository.ts | 53 +- .../repositories/trading-config.repository.ts | 14 + .../src/app/(dashboard)/market-maker/page.tsx | 648 +++++++++ .../src/components/layout/sidebar.tsx | 2 + .../market-maker/api/market-maker.api.ts | 268 ++++ .../market-maker/hooks/use-market-maker.ts | 299 +++++ .../src/features/market-maker/index.ts | 2 + .../pages/trading/trading_page.dart | 23 + 21 files changed, 3434 insertions(+), 8 deletions(-) create mode 100644 backend/services/trading-service/prisma/migrations/0003_add_market_maker_depth/migration.sql create mode 100644 backend/services/trading-service/prisma/migrations/0005_add_market_maker_and_order_source/migration.sql create mode 100644 backend/services/trading-service/src/api/controllers/market-maker.controller.ts create mode 100644 backend/services/trading-service/src/application/services/market-maker.service.ts create mode 100644 frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx create mode 100644 frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts create mode 100644 frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts create mode 100644 frontend/mining-admin-web/src/features/market-maker/index.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4dc5ad2c..783754fa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] diff --git a/backend/services/trading-service/prisma/migrations/0003_add_market_maker_depth/migration.sql b/backend/services/trading-service/prisma/migrations/0003_add_market_maker_depth/migration.sql new file mode 100644 index 00000000..2c9d841d --- /dev/null +++ b/backend/services/trading-service/prisma/migrations/0003_add_market_maker_depth/migration.sql @@ -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 $$; diff --git a/backend/services/trading-service/prisma/migrations/0005_add_market_maker_and_order_source/migration.sql b/backend/services/trading-service/prisma/migrations/0005_add_market_maker_and_order_source/migration.sql new file mode 100644 index 00000000..73506eb2 --- /dev/null +++ b/backend/services/trading-service/prisma/migrations/0005_add_market_maker_and_order_source/migration.sql @@ -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); diff --git a/backend/services/trading-service/prisma/schema.prisma b/backend/services/trading-service/prisma/schema.prisma index 71680ed3..f020ca0c 100644 --- a/backend/services/trading-service/prisma/schema.prisma +++ b/backend/services/trading-service/prisma/schema.prisma @@ -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") +} diff --git a/backend/services/trading-service/src/api/api.module.ts b/backend/services/trading-service/src/api/api.module.ts index a83e0391..cf493213 100644 --- a/backend/services/trading-service/src/api/api.module.ts +++ b/backend/services/trading-service/src/api/api.module.ts @@ -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 {} diff --git a/backend/services/trading-service/src/api/controllers/admin.controller.ts b/backend/services/trading-service/src/api/controllers/admin.controller.ts index e97624f9..f6abbf4c 100644 --- a/backend/services/trading-service/src/api/controllers/admin.controller.ts +++ b/backend/services/trading-service/src/api/controllers/admin.controller.ts @@ -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 ? '深度显示已开启' : '深度显示已关闭', + }; + } } diff --git a/backend/services/trading-service/src/api/controllers/market-maker.controller.ts b/backend/services/trading-service/src/api/controllers/market-maker.controller.ts new file mode 100644 index 00000000..6c3a11b2 --- /dev/null +++ b/backend/services/trading-service/src/api/controllers/market-maker.controller.ts @@ -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, + }; + } +} diff --git a/backend/services/trading-service/src/api/controllers/price.controller.ts b/backend/services/trading-service/src/api/controllers/price.controller.ts index 71b85dfc..bf2bfe8b 100644 --- a/backend/services/trading-service/src/api/controllers/price.controller.ts +++ b/backend/services/trading-service/src/api/controllers/price.controller.ts @@ -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, + }; + } } diff --git a/backend/services/trading-service/src/application/application.module.ts b/backend/services/trading-service/src/application/application.module.ts index 6e1e5121..0d89e29a 100644 --- a/backend/services/trading-service/src/application/application.module.ts +++ b/backend/services/trading-service/src/application/application.module.ts @@ -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 {} diff --git a/backend/services/trading-service/src/application/services/market-maker.service.ts b/backend/services/trading-service/src/application/services/market-maker.service.ts new file mode 100644 index 00000000..2b8a2fa6 --- /dev/null +++ b/backend/services/trading-service/src/application/services/market-maker.service.ts @@ -0,0 +1,1168 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; +import { RedisService } from '../../infrastructure/redis/redis.service'; +import { OrderRepository } from '../../infrastructure/persistence/repositories/order.repository'; +import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository'; +import { OrderService } from './order.service'; +import { PriceService } from './price.service'; +import { OrderType, OrderSource } from '../../domain/aggregates/order.aggregate'; +import { TradingAccountAggregate } from '../../domain/aggregates/trading-account.aggregate'; +import { Money } from '../../domain/value-objects/money.vo'; +import Decimal from 'decimal.js'; + +export interface DepthLevel { + price: string; + quantity: string; + total: string; // 累计数量 +} + +export interface DepthData { + bids: DepthLevel[]; // 买单深度(从高到低) + asks: DepthLevel[]; // 卖单深度(从低到高) + timestamp: number; +} + +export interface MarketMakerConfig { + id: string; + name: string; + accountSequence: string; + cashBalance: Decimal; + shareBalance: Decimal; + frozenCash: Decimal; + frozenShares: Decimal; + maxBuyRatio: Decimal; + minIntervalMs: number; + maxIntervalMs: number; + priceStrategy: string; + discountRate: Decimal; + isActive: boolean; +} + +export enum LedgerType { + DEPOSIT = 'DEPOSIT', + WITHDRAW = 'WITHDRAW', + BUY = 'BUY', + SELL = 'SELL', + FREEZE = 'FREEZE', + UNFREEZE = 'UNFREEZE', +} + +export enum AssetType { + CASH = 'CASH', + SHARE = 'SHARE', +} + +@Injectable() +export class MarketMakerService { + private readonly logger = new Logger(MarketMakerService.name); + private isRunning = false; + private timeoutId: NodeJS.Timeout | null = null; + private makerTimeoutId: NodeJS.Timeout | null = null; + private isMakerRunning = false; + + constructor( + private readonly prisma: PrismaService, + private readonly redis: RedisService, + private readonly orderRepository: OrderRepository, + private readonly accountRepository: TradingAccountRepository, + private readonly orderService: OrderService, + private readonly priceService: PriceService, + ) {} + + /** + * 获取做市商配置 + * 注意:余额从 TradingAccount 获取,保证一致性 + */ + async getConfig(name: string = 'MAIN_MARKET_MAKER'): Promise { + const config = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!config) return null; + + // 从交易账户获取实际余额(保证一致性) + const tradingAccount = await this.accountRepository.findByAccountSequence(config.accountSequence); + const cashBalance = tradingAccount ? new Decimal(tradingAccount.cashBalance.value.toString()) : new Decimal(0); + const shareBalance = tradingAccount ? new Decimal(tradingAccount.shareBalance.value.toString()) : new Decimal(0); + const frozenCash = tradingAccount ? new Decimal(tradingAccount.frozenCash.value.toString()) : new Decimal(0); + const frozenShares = tradingAccount ? new Decimal(tradingAccount.frozenShares.value.toString()) : new Decimal(0); + + return { + id: config.id, + name: config.name, + accountSequence: config.accountSequence, + cashBalance, + shareBalance, + frozenCash, + frozenShares, + maxBuyRatio: new Decimal(config.maxBuyRatio.toString()), + minIntervalMs: config.minIntervalMs, + maxIntervalMs: config.maxIntervalMs, + priceStrategy: config.priceStrategy, + discountRate: new Decimal(config.discountRate.toString()), + isActive: config.isActive, + }; + } + + /** + * 初始化做市商配置(如果不存在) + */ + async initializeConfig(params: { + name?: string; + accountSequence: string; + initialCash?: string; + maxBuyRatio?: number; + minIntervalMs?: number; + maxIntervalMs?: number; + }): Promise { + const name = params.name || 'MAIN_MARKET_MAKER'; + + // 检查是否已存在 + const existing = await this.getConfig(name); + if (existing) { + return existing; + } + + // 确保交易账户存在 + let tradingAccount = await this.accountRepository.findByAccountSequence(params.accountSequence); + if (!tradingAccount) { + tradingAccount = TradingAccountAggregate.create(params.accountSequence); + await this.accountRepository.save(tradingAccount); + } + + // 创建做市商配置 + const config = await this.prisma.marketMakerConfig.create({ + data: { + name, + accountSequence: params.accountSequence, + cashBalance: params.initialCash || '0', + maxBuyRatio: params.maxBuyRatio || 0.05, + minIntervalMs: params.minIntervalMs || 1000, + maxIntervalMs: params.maxIntervalMs || 4000, + }, + }); + + this.logger.log(`Market maker initialized: ${name}, account: ${params.accountSequence}`); + + return this.getConfig(name) as Promise; + } + + /** + * 充值资金到做市商账户 + * 注意:只更新 TradingAccount,不维护双重余额 + */ + async deposit(name: string, amount: string, memo?: string): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + const depositAmount = new Decimal(amount); + if (depositAmount.lte(0)) { + throw new Error('Deposit amount must be positive'); + } + + // 获取交易账户当前余额 + let tradingAccount = await this.accountRepository.findByAccountSequence(configRecord.accountSequence); + if (!tradingAccount) { + tradingAccount = TradingAccountAggregate.create(configRecord.accountSequence); + } + + const balanceBefore = new Decimal(tradingAccount.cashBalance.value.toString()); + const balanceAfter = balanceBefore.plus(depositAmount); + + // 更新交易账户余额 + tradingAccount.deposit(new Money(depositAmount), 'MARKET_MAKER_DEPOSIT'); + await this.accountRepository.save(tradingAccount); + + // 记录做市商分类账 + await this.prisma.marketMakerLedger.create({ + data: { + marketMakerId: configRecord.id, + type: LedgerType.DEPOSIT, + assetType: AssetType.CASH, + amount: depositAmount, + balanceBefore, + balanceAfter, + memo: memo || '管理员充值', + }, + }); + + this.logger.log(`Market maker ${name} deposited: ${amount}, new balance: ${balanceAfter.toString()}`); + } + + /** + * 提现资金 + * 注意:只操作 TradingAccount,不维护双重余额 + */ + async withdraw(name: string, amount: string, memo?: string): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + const withdrawAmount = new Decimal(amount); + + // 获取交易账户当前余额 + const tradingAccount = await this.accountRepository.findByAccountSequence(configRecord.accountSequence); + if (!tradingAccount) { + throw new Error('Trading account not found'); + } + + const availableBalance = new Decimal(tradingAccount.availableCash.value.toString()); + if (withdrawAmount.gt(availableBalance)) { + throw new Error(`Insufficient balance. Available: ${availableBalance.toString()}`); + } + + const balanceBefore = new Decimal(tradingAccount.cashBalance.value.toString()); + const balanceAfter = balanceBefore.minus(withdrawAmount); + + // 更新交易账户余额 + tradingAccount.withdraw(new Money(withdrawAmount), 'MARKET_MAKER_WITHDRAW'); + await this.accountRepository.save(tradingAccount); + + // 记录做市商分类账 + await this.prisma.marketMakerLedger.create({ + data: { + marketMakerId: configRecord.id, + type: LedgerType.WITHDRAW, + assetType: AssetType.CASH, + amount: withdrawAmount.negated(), + balanceBefore, + balanceAfter, + memo: memo || '管理员提现', + }, + }); + + this.logger.log(`Market maker ${name} withdrew: ${amount}`); + } + + /** + * 充值积分股到做市商账户 + */ + async depositShares(name: string, amount: string, memo?: string): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + const depositAmount = new Decimal(amount); + if (depositAmount.lte(0)) { + throw new Error('Deposit amount must be positive'); + } + + // 获取交易账户当前余额 + let tradingAccount = await this.accountRepository.findByAccountSequence(configRecord.accountSequence); + if (!tradingAccount) { + tradingAccount = TradingAccountAggregate.create(configRecord.accountSequence); + } + + const balanceBefore = new Decimal(tradingAccount.shareBalance.value.toString()); + const balanceAfter = balanceBefore.plus(depositAmount); + + // 更新交易账户积分股余额 + tradingAccount.depositShares(new Money(depositAmount), 'MARKET_MAKER_SHARE_DEPOSIT'); + await this.accountRepository.save(tradingAccount); + + // 记录做市商分类账 + await this.prisma.marketMakerLedger.create({ + data: { + marketMakerId: configRecord.id, + type: LedgerType.DEPOSIT, + assetType: AssetType.SHARE, + amount: depositAmount, + balanceBefore, + balanceAfter, + memo: memo || '管理员充值积分股', + }, + }); + + this.logger.log(`Market maker ${name} deposited shares: ${amount}, new balance: ${balanceAfter.toString()}`); + } + + /** + * 提现积分股 + */ + async withdrawShares(name: string, amount: string, memo?: string): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + const withdrawAmount = new Decimal(amount); + + // 获取交易账户当前余额 + const tradingAccount = await this.accountRepository.findByAccountSequence(configRecord.accountSequence); + if (!tradingAccount) { + throw new Error('Trading account not found'); + } + + const availableBalance = new Decimal(tradingAccount.availableShares.value.toString()); + if (withdrawAmount.gt(availableBalance)) { + throw new Error(`Insufficient share balance. Available: ${availableBalance.toString()}`); + } + + const balanceBefore = new Decimal(tradingAccount.shareBalance.value.toString()); + const balanceAfter = balanceBefore.minus(withdrawAmount); + + // 更新交易账户积分股余额 + tradingAccount.withdrawShares(new Money(withdrawAmount), 'MARKET_MAKER_SHARE_WITHDRAW'); + await this.accountRepository.save(tradingAccount); + + // 记录做市商分类账 + await this.prisma.marketMakerLedger.create({ + data: { + marketMakerId: configRecord.id, + type: LedgerType.WITHDRAW, + assetType: AssetType.SHARE, + amount: withdrawAmount.negated(), + balanceBefore, + balanceAfter, + memo: memo || '管理员提现积分股', + }, + }); + + this.logger.log(`Market maker ${name} withdrew shares: ${amount}`); + } + + /** + * 启动做市商 + */ + async start(name: string = 'MAIN_MARKET_MAKER'): Promise { + const config = await this.getConfig(name); + if (!config) { + throw new Error(`Market maker ${name} not found`); + } + + if (this.isRunning) { + this.logger.warn('Market maker is already running'); + return; + } + + await this.prisma.marketMakerConfig.update({ + where: { id: config.id }, + data: { isActive: true }, + }); + + this.isRunning = true; + this.logger.log(`Market maker ${name} started`); + + // 开始吃单循环 + this.scheduleNextRun(config); + } + + /** + * 停止做市商 + */ + async stop(name: string = 'MAIN_MARKET_MAKER'): Promise { + const config = await this.getConfig(name); + if (!config) { + throw new Error(`Market maker ${name} not found`); + } + + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + + this.isRunning = false; + + await this.prisma.marketMakerConfig.update({ + where: { id: config.id }, + data: { isActive: false }, + }); + + this.logger.log(`Market maker ${name} stopped`); + } + + /** + * 执行一次吃单 + */ + async executeTakeOrder(name: string = 'MAIN_MARKET_MAKER'): Promise<{ + success: boolean; + message: string; + tradeInfo?: { + orderNo: string; + sellOrderNo: string; + price: string; + quantity: string; + amount: string; + }; + }> { + const lockValue = await this.redis.acquireLock(`market-maker:take:${name}`, 30); + if (!lockValue) { + return { success: false, message: 'Another take order is in progress' }; + } + + try { + // 获取最新配置 + const config = await this.getConfig(name); + if (!config) { + return { success: false, message: `Market maker ${name} not found` }; + } + + // 查找最老的待成交卖单 + const oldestSellOrder = await this.orderRepository.findOldestPendingSellOrder(); + if (!oldestSellOrder) { + return { success: false, message: 'No pending sell orders found' }; + } + + // 计算可用资金 + const availableCash = config.cashBalance.minus(config.frozenCash); + if (availableCash.lte(0)) { + return { success: false, message: 'Insufficient cash balance' }; + } + + // 计算本次最大买入金额(资金池的 maxBuyRatio) + const maxBuyAmount = availableCash.times(config.maxBuyRatio); + + // 根据价格策略确定买入价格 + let buyPrice: Decimal; + switch (config.priceStrategy) { + case 'DISCOUNT': + buyPrice = new Decimal(oldestSellOrder.price.value).times(config.discountRate); + break; + case 'MARKET': + // TODO: 从价格服务获取当前市场价 + buyPrice = new Decimal(oldestSellOrder.price.value); + break; + case 'TAKER': + default: + // 以卖单价格买入 + buyPrice = new Decimal(oldestSellOrder.price.value); + break; + } + + // 计算买入数量 + const maxQuantityByAmount = maxBuyAmount.dividedBy(buyPrice); + const sellOrderRemaining = new Decimal(oldestSellOrder.remainingQuantity.value); + const buyQuantity = Decimal.min(maxQuantityByAmount, sellOrderRemaining); + + if (buyQuantity.lte(0)) { + return { success: false, message: 'Calculated buy quantity is zero or negative' }; + } + + // 创建买单(OrderService 会自动撮合) + const result = await this.orderService.createOrder( + config.accountSequence, + OrderType.BUY, + buyPrice.toString(), + buyQuantity.toString(), + OrderSource.MARKET_MAKER, + '做市商吃单', + ); + + // 获取实际成交量(可能部分成交或全部成交) + const filledQuantity = new Decimal(result.filledQuantity); + + if (filledQuantity.lte(0)) { + // 订单未成交(可能价格不匹配或其他原因) + this.logger.warn(`Market maker order not filled: ${result.orderNo}`); + return { + success: true, + message: 'Order created but not filled yet', + tradeInfo: { + orderNo: result.orderNo, + sellOrderNo: oldestSellOrder.orderNo, + price: buyPrice.toString(), + quantity: '0', + amount: '0', + }, + }; + } + + // 基于实际成交量记录分类账 + const actualAmount = filledQuantity.times(buyPrice); + await this.recordTradeLedger(config.id, { + type: LedgerType.BUY, + cashAmount: actualAmount, + shareAmount: filledQuantity, + price: buyPrice, + orderNo: result.orderNo, + counterpartySeq: oldestSellOrder.accountSequence, + memo: `吃单买入, 卖单号: ${oldestSellOrder.orderNo}, 成交量: ${filledQuantity.toString()}`, + }); + + // 更新做市商统计(基于实际成交量) + await this.prisma.marketMakerConfig.update({ + where: { id: config.id }, + data: { + lastRunAt: new Date(), + totalBuyCount: { increment: 1 }, + totalBuyQuantity: { increment: filledQuantity }, + totalBuyAmount: { increment: actualAmount }, + }, + }); + + this.logger.log( + `Market maker took order: ${result.orderNo}, price: ${buyPrice.toString()}, filled: ${filledQuantity.toString()}`, + ); + + return { + success: true, + message: 'Order taken successfully', + tradeInfo: { + orderNo: result.orderNo, + sellOrderNo: oldestSellOrder.orderNo, + price: buyPrice.toString(), + quantity: filledQuantity.toString(), + amount: actualAmount.toString(), + }, + }; + } catch (error) { + this.logger.error(`Market maker take order failed: ${error}`); + return { success: false, message: `Take order failed: ${error}` }; + } finally { + await this.redis.releaseLock(`market-maker:take:${name}`, lockValue); + } + } + + /** + * 记录交易分类账 + * 注意:余额已由 OrderService 在 TradingAccount 中更新,这里只记录分类账 + */ + private async recordTradeLedger( + marketMakerId: string, + params: { + type: LedgerType; + cashAmount: Decimal; + shareAmount: Decimal; + price: Decimal; + orderNo: string; + tradeNo?: string; + counterpartySeq?: string; + memo?: string; + }, + ): Promise { + const config = await this.prisma.marketMakerConfig.findUnique({ + where: { id: marketMakerId }, + }); + if (!config) return; + + // 从 TradingAccount 获取实际余额 + const tradingAccount = await this.accountRepository.findByAccountSequence(config.accountSequence); + if (!tradingAccount) return; + + // 注意:此时 OrderService.createOrder 已经完成撮合并更新了 TradingAccount + // 所以 TradingAccount 中的余额是成交后的余额(balanceAfter) + const cashAfter = new Decimal(tradingAccount.cashBalance.value.toString()); + const shareAfter = new Decimal(tradingAccount.shareBalance.value.toString()); + + // 买入:现金减少,积分股增加 + // 卖出:现金增加,积分股减少 + const isBuy = params.type === LedgerType.BUY; + const cashBefore = isBuy ? cashAfter.plus(params.cashAmount) : cashAfter.minus(params.cashAmount); + const shareBefore = isBuy ? shareAfter.minus(params.shareAmount) : shareAfter.plus(params.shareAmount); + + // 只记录分类账,不再更新 MarketMakerConfig 的余额字段 + await this.prisma.$transaction(async (tx) => { + // 记录现金流水 + await tx.marketMakerLedger.create({ + data: { + marketMakerId, + type: params.type, + assetType: AssetType.CASH, + amount: isBuy ? params.cashAmount.negated() : params.cashAmount, + balanceBefore: cashBefore, + balanceAfter: cashAfter, + orderNo: params.orderNo, + tradeNo: params.tradeNo, + counterpartySeq: params.counterpartySeq, + price: params.price, + quantity: params.shareAmount, + memo: params.memo, + }, + }); + + // 记录积分股流水 + await tx.marketMakerLedger.create({ + data: { + marketMakerId, + type: params.type, + assetType: AssetType.SHARE, + amount: isBuy ? params.shareAmount : params.shareAmount.negated(), + balanceBefore: shareBefore, + balanceAfter: shareAfter, + orderNo: params.orderNo, + tradeNo: params.tradeNo, + counterpartySeq: params.counterpartySeq, + price: params.price, + quantity: params.shareAmount, + memo: params.memo, + }, + }); + }); + } + + /** + * 调度下一次运行 + */ + private scheduleNextRun(config: MarketMakerConfig): void { + if (!this.isRunning) return; + + // 随机间隔 (minIntervalMs ~ maxIntervalMs) + const interval = + config.minIntervalMs + Math.random() * (config.maxIntervalMs - config.minIntervalMs); + + this.timeoutId = setTimeout(async () => { + await this.executeTakeOrder(config.name); + + // 重新获取配置(可能已被更新) + const latestConfig = await this.getConfig(config.name); + if (latestConfig && latestConfig.isActive) { + this.scheduleNextRun(latestConfig); + } else { + this.isRunning = false; + } + }, interval); + } + + /** + * 获取做市商统计 + */ + async getStats(name: string = 'MAIN_MARKET_MAKER'): Promise<{ + config: MarketMakerConfig | null; + recentTrades: any[]; + dailyStats: any[]; + }> { + const config = await this.getConfig(name); + if (!config) { + return { config: null, recentTrades: [], dailyStats: [] }; + } + + const [recentTrades, dailyStats] = await Promise.all([ + this.prisma.marketMakerLedger.findMany({ + where: { + marketMakerId: config.id, + type: { in: [LedgerType.BUY, LedgerType.SELL] }, + }, + orderBy: { createdAt: 'desc' }, + take: 20, + }), + this.prisma.marketMakerDailyStats.findMany({ + where: { marketMakerId: config.id }, + orderBy: { date: 'desc' }, + take: 7, + }), + ]); + + return { config, recentTrades, dailyStats }; + } + + /** + * 获取分类账列表 + */ + async getLedgers( + name: string, + options?: { + type?: LedgerType; + assetType?: AssetType; + page?: number; + pageSize?: number; + }, + ): Promise<{ data: any[]; total: number }> { + const config = await this.getConfig(name); + if (!config) { + return { data: [], total: 0 }; + } + + const where: any = { marketMakerId: config.id }; + if (options?.type) where.type = options.type; + if (options?.assetType) where.assetType = options.assetType; + + const page = options?.page ?? 1; + const pageSize = options?.pageSize ?? 50; + + const [data, total] = await Promise.all([ + this.prisma.marketMakerLedger.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.marketMakerLedger.count({ where }), + ]); + + return { data, total }; + } + + /** + * 更新配置 + */ + async updateConfig( + name: string, + updates: { + maxBuyRatio?: number; + minIntervalMs?: number; + maxIntervalMs?: number; + priceStrategy?: string; + discountRate?: number; + }, + ): Promise { + const config = await this.getConfig(name); + if (!config) { + throw new Error(`Market maker ${name} not found`); + } + + await this.prisma.marketMakerConfig.update({ + where: { id: config.id }, + data: updates, + }); + + return this.getConfig(name); + } + + // ============ 双边挂单(深度做市)相关方法 ============ + + /** + * 启动挂单模式(双边深度) + */ + async startMaker(name: string = 'MAIN_MARKET_MAKER'): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + if (this.isMakerRunning) { + this.logger.warn('Maker mode is already running'); + return; + } + + await this.prisma.marketMakerConfig.update({ + where: { id: configRecord.id }, + data: { makerEnabled: true }, + }); + + this.isMakerRunning = true; + this.logger.log(`Market maker ${name} maker mode started`); + + // 立即刷新一次挂单 + await this.refreshMakerOrders(name); + + // 开始定时刷新循环 + this.scheduleMakerRefresh(name, configRecord.refreshIntervalMs); + } + + /** + * 停止挂单模式 + */ + async stopMaker(name: string = 'MAIN_MARKET_MAKER'): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + if (this.makerTimeoutId) { + clearTimeout(this.makerTimeoutId); + this.makerTimeoutId = null; + } + + this.isMakerRunning = false; + + // 取消所有做市商挂单 + await this.cancelAllMakerOrders(name); + + await this.prisma.marketMakerConfig.update({ + where: { id: configRecord.id }, + data: { makerEnabled: false }, + }); + + this.logger.log(`Market maker ${name} maker mode stopped`); + } + + /** + * 刷新双边挂单 + */ + async refreshMakerOrders(name: string = 'MAIN_MARKET_MAKER'): Promise<{ + success: boolean; + message: string; + bidOrders?: number; + askOrders?: number; + }> { + const lockValue = await this.redis.acquireLock(`market-maker:maker:${name}`, 60); + if (!lockValue) { + return { success: false, message: 'Another refresh is in progress' }; + } + + try { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + return { success: false, message: `Market maker ${name} not found` }; + } + + // 获取当前价格 + const priceInfo = await this.priceService.getCurrentPrice(); + const currentPrice = new Decimal(priceInfo.price); + + if (currentPrice.lte(0)) { + return { success: false, message: 'Invalid current price' }; + } + + // 获取交易账户余额 + const tradingAccount = await this.accountRepository.findByAccountSequence(configRecord.accountSequence); + if (!tradingAccount) { + return { success: false, message: 'Trading account not found' }; + } + + const availableCash = new Decimal(tradingAccount.availableCash.value.toString()); + const availableShares = new Decimal(tradingAccount.availableShares.value.toString()); + + // 先取消所有现有的做市商挂单 + await this.cancelAllMakerOrders(name); + + let bidOrdersCreated = 0; + let askOrdersCreated = 0; + + // 创建买单(Bid) + if (configRecord.bidEnabled && availableCash.gt(0)) { + const bidSpread = new Decimal(configRecord.bidSpread.toString()); + const bidLevelSpacing = new Decimal(configRecord.bidLevelSpacing.toString()); + const bidQuantityPerLevel = new Decimal(configRecord.bidQuantityPerLevel.toString()); + + for (let level = 1; level <= configRecord.bidLevels; level++) { + // 价格 = 当前价 * (1 - spread - (level-1) * spacing) + const priceDiscount = bidSpread.plus(bidLevelSpacing.times(level - 1)); + const bidPrice = currentPrice.times(new Decimal(1).minus(priceDiscount)); + + // 计算该档位需要的资金 + const requiredCash = bidPrice.times(bidQuantityPerLevel); + + // 检查资金是否足够 + const remainingCash = availableCash.minus( + bidPrice.times(bidQuantityPerLevel).times(bidOrdersCreated), + ); + if (remainingCash.lt(requiredCash)) { + this.logger.warn(`Insufficient cash for bid level ${level}`); + break; + } + + try { + // 创建买单 + const result = await this.orderService.createOrder( + configRecord.accountSequence, + OrderType.BUY, + bidPrice.toFixed(18), + bidQuantityPerLevel.toFixed(8), + OrderSource.MARKET_MAKER, + `做市商买单 L${level}`, + ); + + // 记录做市商挂单 + await this.prisma.marketMakerOrder.create({ + data: { + marketMakerId: configRecord.id, + orderId: result.orderId, + orderNo: result.orderNo, + side: 'BID', + level, + price: bidPrice, + quantity: bidQuantityPerLevel, + remainingQty: bidQuantityPerLevel, + status: 'ACTIVE', + }, + }); + + bidOrdersCreated++; + } catch (error) { + this.logger.error(`Failed to create bid order at level ${level}: ${error}`); + } + } + } + + // 创建卖单(Ask) + if (configRecord.askEnabled && availableShares.gt(0)) { + const askSpread = new Decimal(configRecord.askSpread.toString()); + const askLevelSpacing = new Decimal(configRecord.askLevelSpacing.toString()); + const askQuantityPerLevel = new Decimal(configRecord.askQuantityPerLevel.toString()); + + for (let level = 1; level <= configRecord.askLevels; level++) { + // 价格 = 当前价 * (1 + spread + (level-1) * spacing) + const pricePremium = askSpread.plus(askLevelSpacing.times(level - 1)); + const askPrice = currentPrice.times(new Decimal(1).plus(pricePremium)); + + // 检查积分股是否足够 + const usedShares = askQuantityPerLevel.times(askOrdersCreated); + const remainingShares = availableShares.minus(usedShares); + if (remainingShares.lt(askQuantityPerLevel)) { + this.logger.warn(`Insufficient shares for ask level ${level}`); + break; + } + + try { + // 创建卖单 + const result = await this.orderService.createOrder( + configRecord.accountSequence, + OrderType.SELL, + askPrice.toFixed(18), + askQuantityPerLevel.toFixed(8), + OrderSource.MARKET_MAKER, + `做市商卖单 L${level}`, + ); + + // 记录做市商挂单 + await this.prisma.marketMakerOrder.create({ + data: { + marketMakerId: configRecord.id, + orderId: result.orderId, + orderNo: result.orderNo, + side: 'ASK', + level, + price: askPrice, + quantity: askQuantityPerLevel, + remainingQty: askQuantityPerLevel, + status: 'ACTIVE', + }, + }); + + askOrdersCreated++; + } catch (error) { + this.logger.error(`Failed to create ask order at level ${level}: ${error}`); + } + } + } + + // 更新最后刷新时间 + await this.prisma.marketMakerConfig.update({ + where: { id: configRecord.id }, + data: { lastRefreshAt: new Date() }, + }); + + this.logger.log( + `Market maker orders refreshed: ${bidOrdersCreated} bids, ${askOrdersCreated} asks`, + ); + + return { + success: true, + message: 'Orders refreshed', + bidOrders: bidOrdersCreated, + askOrders: askOrdersCreated, + }; + } catch (error) { + this.logger.error(`Failed to refresh maker orders: ${error}`); + return { success: false, message: `Refresh failed: ${error}` }; + } finally { + await this.redis.releaseLock(`market-maker:maker:${name}`, lockValue); + } + } + + /** + * 取消所有做市商挂单 + */ + async cancelAllMakerOrders(name: string = 'MAIN_MARKET_MAKER'): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + return 0; + } + + // 查找所有活跃的做市商挂单 + const activeOrders = await this.prisma.marketMakerOrder.findMany({ + where: { + marketMakerId: configRecord.id, + status: 'ACTIVE', + }, + }); + + let cancelledCount = 0; + + for (const makerOrder of activeOrders) { + try { + // 取消订单 + await this.orderService.cancelOrder(makerOrder.orderNo, configRecord.accountSequence); + + // 更新做市商挂单状态 + await this.prisma.marketMakerOrder.update({ + where: { id: makerOrder.id }, + data: { status: 'CANCELLED' }, + }); + + cancelledCount++; + } catch (error) { + this.logger.error(`Failed to cancel maker order ${makerOrder.orderNo}: ${error}`); + } + } + + this.logger.log(`Cancelled ${cancelledCount} maker orders`); + return cancelledCount; + } + + /** + * 获取深度数据 + */ + async getDepth(levels: number = 10): Promise { + // 获取所有活跃的买单(按价格从高到低) + const buyOrders = await this.prisma.order.findMany({ + where: { + type: 'BUY', + status: { in: ['PENDING', 'PARTIAL'] }, + }, + orderBy: { price: 'desc' }, + }); + + // 获取所有活跃的卖单(按价格从低到高) + const sellOrders = await this.prisma.order.findMany({ + where: { + type: 'SELL', + status: { in: ['PENDING', 'PARTIAL'] }, + }, + orderBy: { price: 'asc' }, + }); + + // 聚合买单深度 + const bidMap = new Map(); + for (const order of buyOrders) { + const priceKey = new Decimal(order.price.toString()).toFixed(8); + const existing = bidMap.get(priceKey) || new Decimal(0); + bidMap.set(priceKey, existing.plus(new Decimal(order.remainingQuantity.toString()))); + } + + // 聚合卖单深度 + const askMap = new Map(); + for (const order of sellOrders) { + const priceKey = new Decimal(order.price.toString()).toFixed(8); + const existing = askMap.get(priceKey) || new Decimal(0); + askMap.set(priceKey, existing.plus(new Decimal(order.remainingQuantity.toString()))); + } + + // 转换为深度格式 + const bids: DepthLevel[] = []; + let bidTotal = new Decimal(0); + const bidPrices = Array.from(bidMap.keys()).sort((a, b) => new Decimal(b).minus(new Decimal(a)).toNumber()); + for (const price of bidPrices.slice(0, levels)) { + const quantity = bidMap.get(price)!; + bidTotal = bidTotal.plus(quantity); + bids.push({ + price, + quantity: quantity.toFixed(8), + total: bidTotal.toFixed(8), + }); + } + + const asks: DepthLevel[] = []; + let askTotal = new Decimal(0); + const askPrices = Array.from(askMap.keys()).sort((a, b) => new Decimal(a).minus(new Decimal(b)).toNumber()); + for (const price of askPrices.slice(0, levels)) { + const quantity = askMap.get(price)!; + askTotal = askTotal.plus(quantity); + asks.push({ + price, + quantity: quantity.toFixed(8), + total: askTotal.toFixed(8), + }); + } + + return { + bids, + asks, + timestamp: Date.now(), + }; + } + + /** + * 更新挂单配置 + */ + async updateMakerConfig( + name: string, + updates: { + bidEnabled?: boolean; + bidLevels?: number; + bidSpread?: number; + bidLevelSpacing?: number; + bidQuantityPerLevel?: string; + askEnabled?: boolean; + askLevels?: number; + askSpread?: number; + askLevelSpacing?: number; + askQuantityPerLevel?: string; + refreshIntervalMs?: number; + }, + ): Promise { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + throw new Error(`Market maker ${name} not found`); + } + + await this.prisma.marketMakerConfig.update({ + where: { id: configRecord.id }, + data: updates, + }); + + this.logger.log(`Market maker ${name} maker config updated`); + } + + /** + * 获取做市商挂单列表 + */ + async getMakerOrders( + name: string, + options?: { + side?: 'BID' | 'ASK'; + status?: 'ACTIVE' | 'FILLED' | 'CANCELLED'; + page?: number; + pageSize?: number; + }, + ): Promise<{ data: any[]; total: number }> { + const configRecord = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (!configRecord) { + return { data: [], total: 0 }; + } + + const where: any = { marketMakerId: configRecord.id }; + if (options?.side) where.side = options.side; + if (options?.status) where.status = options.status; + + const page = options?.page ?? 1; + const pageSize = options?.pageSize ?? 50; + + const [data, total] = await Promise.all([ + this.prisma.marketMakerOrder.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.marketMakerOrder.count({ where }), + ]); + + return { data, total }; + } + + /** + * 调度下一次挂单刷新 + */ + private scheduleMakerRefresh(name: string, intervalMs: number): void { + if (!this.isMakerRunning) return; + + this.makerTimeoutId = setTimeout(async () => { + await this.refreshMakerOrders(name); + + // 重新获取配置(可能已被更新) + const latestConfig = await this.prisma.marketMakerConfig.findUnique({ + where: { name }, + }); + if (latestConfig && latestConfig.makerEnabled) { + this.scheduleMakerRefresh(name, latestConfig.refreshIntervalMs); + } else { + this.isMakerRunning = false; + } + }, intervalMs); + } +} diff --git a/backend/services/trading-service/src/application/services/order.service.ts b/backend/services/trading-service/src/application/services/order.service.ts index 7633ebd9..a87caba5 100644 --- a/backend/services/trading-service/src/application/services/order.service.ts +++ b/backend/services/trading-service/src/application/services/order.service.ts @@ -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, }, }); diff --git a/backend/services/trading-service/src/domain/aggregates/order.aggregate.ts b/backend/services/trading-service/src/domain/aggregates/order.aggregate.ts index efebbde6..be975664 100644 --- a/backend/services/trading-service/src/domain/aggregates/order.aggregate.ts +++ b/backend/services/trading-service/src/domain/aggregates/order.aggregate.ts @@ -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; } diff --git a/backend/services/trading-service/src/domain/aggregates/trading-account.aggregate.ts b/backend/services/trading-service/src/domain/aggregates/trading-account.aggregate.ts index e9438d77..4cf21234 100644 --- a/backend/services/trading-service/src/domain/aggregates/trading-account.aggregate.ts +++ b/backend/services/trading-service/src/domain/aggregates/trading-account.aggregate.ts @@ -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 = []; } diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts index 1ddb07d4..ebcfa275 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts @@ -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 { + 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), diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts index 952dcba5..8e1753f9 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts @@ -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 { + 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, diff --git a/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx new file mode 100644 index 00000000..13894129 --- /dev/null +++ b/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx @@ -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 ( +
+
+ + +
+ + {/* 未初始化状态 */} + {!configLoading && !config && ( + + + 初始化做市商 + 做市商尚未初始化,请先创建做市商配置 + + +
+
+ + setInitAccountSeq(e.target.value)} + placeholder="做市商专用账户序列号" + /> +
+ +
+
+
+ )} + + {configLoading && ( +
+ + +
+ )} + + {config && ( + <> + {/* 资金状态卡片 */} +
+ {/* 现金余额 */} + + +
+ + + 现金余额(积分值) + +
+
+ +
+
+
+

总余额

+

{formatNumber(config.cashBalance, 2)}

+
+
+

可用余额

+

{formatNumber(config.availableCash, 2)}

+
+
+

冻结中

+

{formatNumber(config.frozenCash, 2)}

+
+
+ +
+ + + + + + + 充值现金 + 向做市商账户充值积分值 + +
+ + setDepositCashAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ + + +
+
+ + + + + + + + 提现现金 + 从做市商账户提取积分值 + +
+ + setWithdrawCashAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ + + +
+
+
+
+
+
+ + {/* 积分股余额 */} + + +
+ + + 积分股余额 + +
+
+ +
+
+
+

总余额

+

{formatNumber(config.shareBalance, 2)}

+
+
+

可用余额

+

{formatNumber(config.availableShares, 2)}

+
+
+

冻结中

+

{formatNumber(config.frozenShares, 2)}

+
+
+ +
+ + + + + + + 充值积分股 + 向做市商账户充值积分股(用于挂卖单) + +
+ + setDepositSharesAmount(e.target.value)} + placeholder="请输入数量" + /> +
+ + + +
+
+ + + + + + + + 提取积分股 + 从做市商账户提取积分股 + +
+ + setWithdrawSharesAmount(e.target.value)} + placeholder="请输入数量" + /> +
+ + + +
+
+
+
+
+
+
+ + {/* 运行模式控制 */} + + + 吃单模式 + 挂单模式(深度) + 深度显示 + + + {/* 吃单模式 */} + + + +
+
+ + + 吃单模式 + + 自动买入用户卖单(Taker 策略) +
+ {config.isActive ? ( + + + 运行中 + + ) : ( + + + 已停止 + + )} +
+
+ +
+
+
+

单次最大买入比例

+

{formatNumber(String(parseFloat(config.maxBuyRatio) * 100), 2)}%

+
+
+

最小间隔

+

{config.minIntervalMs}ms

+
+
+

最大间隔

+

{config.maxIntervalMs}ms

+
+
+

价格策略

+

{config.priceStrategy}

+
+
+ +
+ {config.isActive ? ( + + ) : ( + + )} + +
+
+
+
+
+ + {/* 挂单模式 */} + + + +
+
+ + + 挂单模式(双边深度) + + 自动挂买卖单形成深度(Maker 策略) +
+ {config.makerEnabled ? ( + + + 运行中 + + ) : ( + + + 已停止 + + )} +
+
+ +
+
+
+

买单档位

+

{config.bidLevels || 5}

+
+
+

买单价差

+

{formatNumber(String(parseFloat(config.bidSpread || '0.01') * 100), 2)}%

+
+
+

每档买单量

+

{formatNumber(config.bidQuantityPerLevel || '1000', 0)}

+
+
+

卖单档位

+

{config.askLevels || 5}

+
+
+

卖单价差

+

{formatNumber(String(parseFloat(config.askSpread || '0.01') * 100), 2)}%

+
+
+

每档卖单量

+

{formatNumber(config.askQuantityPerLevel || '1000', 0)}

+
+
+ +
+ {config.makerEnabled ? ( + + ) : ( + + )} + + +
+
+
+
+
+ + {/* 深度显示控制 */} + +
+ {/* 深度开关 */} + + +
+
+ 深度显示开关 + 控制App端是否显示买卖深度 +
+ {depthEnabledLoading ? ( + + ) : depthEnabled?.enabled ? ( + + + 已开启 + + ) : ( + + + 已关闭 + + )} +
+
+ +
+
+

深度显示

+

+ {depthEnabled?.enabled ? '用户可以在App中查看买卖深度' : '深度功能已关闭,用户无法查看'} +

+
+ setDepthEnabledMutation.mutate(checked)} + disabled={setDepthEnabledMutation.isPending || depthEnabledLoading} + /> +
+
+
+ + {/* 当前深度 */} + + + 当前深度 + + + {depthLoading ? ( + + ) : ( +
+ {/* 买单深度 */} +
+

+ + 买单深度 +

+ {depthData?.bids?.length ? ( + + + + 价格 + 数量 + 累计 + + + + {depthData.bids.map((bid, idx) => ( + + {formatNumber(bid.price, 8)} + {formatNumber(bid.quantity, 2)} + {formatNumber(bid.total, 2)} + + ))} + +
+ ) : ( +

暂无买单

+ )} +
+ + {/* 卖单深度 */} +
+

+ + 卖单深度 +

+ {depthData?.asks?.length ? ( + + + + 价格 + 数量 + 累计 + + + + {depthData.asks.map((ask, idx) => ( + + {formatNumber(ask.price, 8)} + {formatNumber(ask.quantity, 2)} + {formatNumber(ask.total, 2)} + + ))} + +
+ ) : ( +

暂无卖单

+ )} +
+
+ )} +
+
+
+
+
+ + )} +
+ ); +} diff --git a/frontend/mining-admin-web/src/components/layout/sidebar.tsx b/frontend/mining-admin-web/src/components/layout/sidebar.tsx index 7cc87965..6cf081af 100644 --- a/frontend/mining-admin-web/src/components/layout/sidebar.tsx +++ b/frontend/mining-admin-web/src/components/layout/sidebar.tsx @@ -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 }, diff --git a/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts b/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts new file mode 100644 index 00000000..46e98c74 --- /dev/null +++ b/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts @@ -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; + 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; + }, +}; diff --git a/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts b/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts new file mode 100644 index 00000000..098ab734 --- /dev/null +++ b/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts @@ -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' }); + }, + }); +} diff --git a/frontend/mining-admin-web/src/features/market-maker/index.ts b/frontend/mining-admin-web/src/features/market-maker/index.ts new file mode 100644 index 00000000..c266e5d3 --- /dev/null +++ b/frontend/mining-admin-web/src/features/market-maker/index.ts @@ -0,0 +1,2 @@ +export * from './api/market-maker.api'; +export * from './hooks/use-market-maker'; diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index e1187a38..ce795ed7 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -563,6 +563,29 @@ class _TradingPageState extends ConsumerState { ], ), ), + 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(