/** * 预测市场服务 * 核心功能:话题管理、席位交易、动态定价、领主系统、奖池分配 */ import { addCredits, deductCredits, canAfford, addPosition, removePosition, updatePosition, getUserPositions, recordWin, recordLoss, CREDIT_CONFIG, } from './creditSystemService'; // ==================== 常量配置 ==================== export const MARKET_CONFIG = { MAX_SEATS_PER_SIDE: 5, // 每个方向最多5个席位 TAX_RATE: 0.02, // 交易税率 2% MIN_PRICE: 50, // 最低价格 MAX_PRICE: 950, // 最高价格 BASE_PRICE: 500, // 基础价格 }; // 话题存储(生产环境应使用Elasticsearch) const topics = new Map(); // 席位存储 const positions = new Map(); // 交易记录 const trades = []; // ==================== 动态定价算法 ==================== /** * 计算当前价格(简化版AMM) * @param {number} yesShares - Yes方总份额 * @param {number} noShares - No方总份额 * @returns {Object} {yes: price, no: price} */ export const calculatePrice = (yesShares, noShares) => { const totalShares = yesShares + noShares; if (totalShares === 0) { // 初始状态,双方价格相同 return { yes: MARKET_CONFIG.BASE_PRICE, no: MARKET_CONFIG.BASE_PRICE, }; } // 概率加权定价 const yesProb = yesShares / totalShares; const noProb = noShares / totalShares; // 价格 = 概率 * 1000,限制在 [MIN_PRICE, MAX_PRICE] const yesPrice = Math.max( MARKET_CONFIG.MIN_PRICE, Math.min(MARKET_CONFIG.MAX_PRICE, yesProb * 1000) ); const noPrice = Math.max( MARKET_CONFIG.MIN_PRICE, Math.min(MARKET_CONFIG.MAX_PRICE, noProb * 1000) ); return { yes: yesPrice, no: noPrice }; }; /** * 计算购买成本(含滑点) * @param {number} currentShares - 当前份额 * @param {number} otherShares - 对手方份额 * @param {number} buyAmount - 购买数量 * @returns {number} 总成本 */ export const calculateBuyCost = (currentShares, otherShares, buyAmount) => { let totalCost = 0; let tempShares = currentShares; // 模拟逐步购买,累计成本 for (let i = 0; i < buyAmount; i++) { tempShares += 1; const prices = calculatePrice(tempShares, otherShares); // 假设购买的是yes方 totalCost += prices.yes; } return totalCost; }; /** * 计算卖出收益(含滑点) * @param {number} currentShares - 当前份额 * @param {number} otherShares - 对手方份额 * @param {number} sellAmount - 卖出数量 * @returns {number} 总收益 */ export const calculateSellRevenue = (currentShares, otherShares, sellAmount) => { let totalRevenue = 0; let tempShares = currentShares; // 模拟逐步卖出,累计收益 for (let i = 0; i < sellAmount; i++) { const prices = calculatePrice(tempShares, otherShares); totalRevenue += prices.yes; tempShares -= 1; } return totalRevenue; }; /** * 计算交易税 * @param {number} amount - 交易金额 * @returns {number} 税费 */ export const calculateTax = (amount) => { return Math.floor(amount * MARKET_CONFIG.TAX_RATE); }; // ==================== 话题管理 ==================== /** * 创建预测话题 * @param {Object} topicData - 话题数据 * @returns {Object} 创建的话题 */ export const createTopic = (topicData) => { const { author_id, title, description, category, tags, deadline, settlement_date } = topicData; // 扣除创建费用 deductCredits(author_id, CREDIT_CONFIG.CREATE_TOPIC_COST, '创建预测话题'); const topic = { id: `topic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'prediction', // 基础信息 title, description, category, tags: tags || [], // 作者信息 author_id, author_name: topicData.author_name, author_avatar: topicData.author_avatar, // 时间管理 created_at: new Date().toISOString(), deadline: deadline || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 默认7天 settlement_date: settlement_date || new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(), status: 'active', // 预测选项 options: [ { id: 'yes', label: '看涨 / Yes', color: '#48BB78' }, { id: 'no', label: '看跌 / No', color: '#F56565' }, ], // 市场数据 total_pool: CREDIT_CONFIG.CREATE_TOPIC_COST, // 创建费用进入奖池 tax_rate: MARKET_CONFIG.TAX_RATE, // 席位数据 positions: { yes: { seats: [], total_shares: 0, current_price: MARKET_CONFIG.BASE_PRICE, lord_id: null, }, no: { seats: [], total_shares: 0, current_price: MARKET_CONFIG.BASE_PRICE, lord_id: null, }, }, // 交易统计 stats: { total_volume: 0, total_transactions: 0, unique_traders: new Set(), }, // 结果 settlement: { result: null, evidence: null, settled_by: null, settled_at: null, }, }; topics.set(topic.id, topic); return topic; }; /** * 获取话题详情 * @param {string} topicId - 话题ID * @returns {Object} 话题详情 */ export const getTopic = (topicId) => { return topics.get(topicId); }; /** * 更新话题 * @param {string} topicId - 话题ID * @param {Object} updates - 更新内容 */ export const updateTopic = (topicId, updates) => { const topic = getTopic(topicId); const updated = { ...topic, ...updates }; topics.set(topicId, updated); return updated; }; /** * 获取所有话题列表 * @param {Object} filters - 筛选条件 * @returns {Array} 话题列表 */ export const getTopics = (filters = {}) => { let topicList = Array.from(topics.values()); // 按状态筛选 if (filters.status) { topicList = topicList.filter((t) => t.status === filters.status); } // 按分类筛选 if (filters.category) { topicList = topicList.filter((t) => t.category === filters.category); } // 排序 const sortBy = filters.sortBy || 'created_at'; topicList.sort((a, b) => { if (sortBy === 'created_at') { return new Date(b.created_at) - new Date(a.created_at); } if (sortBy === 'total_pool') { return b.total_pool - a.total_pool; } if (sortBy === 'total_volume') { return b.stats.total_volume - a.stats.total_volume; } return 0; }); return topicList; }; // ==================== 席位管理 ==================== /** * 创建席位 * @param {Object} positionData - 席位数据 * @returns {Object} 创建的席位 */ const createPosition = (positionData) => { const position = { id: `pos_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ...positionData, acquired_at: new Date().toISOString(), last_traded_at: new Date().toISOString(), is_lord: false, }; positions.set(position.id, position); return position; }; /** * 获取席位 * @param {string} positionId - 席位ID * @returns {Object} 席位信息 */ export const getPosition = (positionId) => { return positions.get(positionId); }; /** * 分配席位(取份额最高的前5名) * @param {Array} allPositions - 所有持仓 * @returns {Array} 席位列表 */ const allocateSeats = (allPositions) => { // 按份额排序 const sorted = [...allPositions].sort((a, b) => b.shares - a.shares); // 取前5名 return sorted.slice(0, MARKET_CONFIG.MAX_SEATS_PER_SIDE); }; /** * 确定领主(份额最多的人) * @param {Array} seats - 席位列表 * @returns {string|null} 领主用户ID */ const determineLord = (seats) => { if (seats.length === 0) return null; const lord = seats.reduce((max, seat) => (seat.shares > max.shares ? seat : max)); return lord.holder_id; }; /** * 更新领主标识 * @param {string} topicId - 话题ID * @param {string} optionId - 选项ID */ const updateLordStatus = (topicId, optionId) => { const topic = getTopic(topicId); const sideData = topic.positions[optionId]; // 重新分配席位 const allPositions = Array.from(positions.values()).filter( (p) => p.topic_id === topicId && p.option_id === optionId ); const seats = allocateSeats(allPositions); const lordId = determineLord(seats); // 更新所有席位的领主标识 allPositions.forEach((position) => { const isLord = position.holder_id === lordId; positions.set(position.id, { ...position, is_lord: isLord }); }); // 更新话题数据 updateTopic(topicId, { positions: { ...topic.positions, [optionId]: { ...sideData, seats, lord_id: lordId, }, }, }); return lordId; }; // ==================== 交易执行 ==================== /** * 购买席位 * @param {Object} tradeData - 交易数据 * @returns {Object} 交易结果 */ export const buyPosition = (tradeData) => { const { user_id, user_name, user_avatar, topic_id, option_id, shares } = tradeData; // 验证 const topic = getTopic(topic_id); if (!topic) throw new Error('话题不存在'); if (topic.status !== 'active') throw new Error('话题已关闭交易'); if (topic.author_id === user_id) throw new Error('不能参与自己发起的话题'); // 检查购买上限 if (shares * MARKET_CONFIG.BASE_PRICE > CREDIT_CONFIG.MAX_SINGLE_BET) { throw new Error(`单次购买上限为${CREDIT_CONFIG.MAX_SINGLE_BET}积分`); } // 获取当前市场数据 const sideData = topic.positions[option_id]; const otherOptionId = option_id === 'yes' ? 'no' : 'yes'; const otherSideData = topic.positions[otherOptionId]; // 计算成本 const cost = calculateBuyCost(sideData.total_shares, otherSideData.total_shares, shares); const tax = calculateTax(cost); const totalCost = cost + tax; // 检查余额 if (!canAfford(user_id, totalCost)) { throw new Error(`积分不足,需要${totalCost}积分`); } // 扣除积分 deductCredits(user_id, totalCost, `购买预测席位 - ${topic.title}`); // 税费进入奖池 updateTopic(topic_id, { total_pool: topic.total_pool + tax, stats: { ...topic.stats, total_volume: topic.stats.total_volume + totalCost, total_transactions: topic.stats.total_transactions + 1, unique_traders: topic.stats.unique_traders.add(user_id), }, }); // 查找用户是否已有该选项的席位 let userPosition = Array.from(positions.values()).find( (p) => p.topic_id === topic_id && p.option_id === option_id && p.holder_id === user_id ); if (userPosition) { // 更新现有席位 const newShares = userPosition.shares + shares; const newAvgCost = (userPosition.avg_cost * userPosition.shares + cost) / newShares; positions.set(userPosition.id, { ...userPosition, shares: newShares, avg_cost: newAvgCost, last_traded_at: new Date().toISOString(), }); // 更新用户账户持仓 updatePosition(user_id, userPosition.id, { shares: newShares, avg_cost: newAvgCost, }); } else { // 创建新席位 const newPosition = createPosition({ topic_id, option_id, holder_id: user_id, holder_name: user_name, holder_avatar: user_avatar, shares, avg_cost: cost / shares, current_value: cost, unrealized_pnl: 0, }); // 添加到用户账户 addPosition(user_id, { id: newPosition.id, topic_id, option_id, shares, avg_cost: cost / shares, }); userPosition = newPosition; } // 更新话题席位数据 updateTopic(topic_id, { positions: { ...topic.positions, [option_id]: { ...sideData, total_shares: sideData.total_shares + shares, }, }, }); // 更新价格 const newPrices = calculatePrice( topic.positions[option_id].total_shares + shares, topic.positions[otherOptionId].total_shares ); updateTopic(topic_id, { positions: { ...topic.positions, yes: { ...topic.positions.yes, current_price: newPrices.yes }, no: { ...topic.positions.no, current_price: newPrices.no }, }, }); // 更新领主状态 const newLordId = updateLordStatus(topic_id, option_id); // 记录交易 const trade = { id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, topic_id, option_id, buyer_id: user_id, seller_id: null, type: 'buy', shares, price: cost / shares, total_cost: totalCost, tax, created_at: new Date().toISOString(), }; trades.push(trade); return { success: true, position: userPosition, trade, new_lord_id: newLordId, current_price: newPrices[option_id], }; }; /** * 卖出席位 * @param {Object} tradeData - 交易数据 * @returns {Object} 交易结果 */ export const sellPosition = (tradeData) => { const { user_id, topic_id, option_id, shares } = tradeData; // 验证 const topic = getTopic(topic_id); if (!topic) throw new Error('话题不存在'); if (topic.status !== 'active') throw new Error('话题已关闭交易'); // 查找用户席位 const userPosition = Array.from(positions.values()).find( (p) => p.topic_id === topic_id && p.option_id === option_id && p.holder_id === user_id ); if (!userPosition) throw new Error('未持有该席位'); if (userPosition.shares < shares) throw new Error('持有份额不足'); // 获取当前市场数据 const sideData = topic.positions[option_id]; const otherOptionId = option_id === 'yes' ? 'no' : 'yes'; const otherSideData = topic.positions[otherOptionId]; // 计算收益 const revenue = calculateSellRevenue(sideData.total_shares, otherSideData.total_shares, shares); const tax = calculateTax(revenue); const netRevenue = revenue - tax; // 返还积分 addCredits(user_id, netRevenue, `卖出预测席位 - ${topic.title}`); // 税费进入奖池 updateTopic(topic_id, { total_pool: topic.total_pool + tax, stats: { ...topic.stats, total_volume: topic.stats.total_volume + revenue, total_transactions: topic.stats.total_transactions + 1, }, }); // 更新席位 const newShares = userPosition.shares - shares; if (newShares === 0) { // 完全卖出,删除席位 positions.delete(userPosition.id); removePosition(user_id, userPosition.id); } else { // 部分卖出,更新份额 positions.set(userPosition.id, { ...userPosition, shares: newShares, last_traded_at: new Date().toISOString(), }); updatePosition(user_id, userPosition.id, { shares: newShares }); } // 更新话题席位数据 updateTopic(topic_id, { positions: { ...topic.positions, [option_id]: { ...sideData, total_shares: sideData.total_shares - shares, }, }, }); // 更新价格 const newPrices = calculatePrice( topic.positions[option_id].total_shares - shares, topic.positions[otherOptionId].total_shares ); updateTopic(topic_id, { positions: { ...topic.positions, yes: { ...topic.positions.yes, current_price: newPrices.yes }, no: { ...topic.positions.no, current_price: newPrices.no }, }, }); // 更新领主状态 const newLordId = updateLordStatus(topic_id, option_id); // 记录交易 const trade = { id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, topic_id, option_id, buyer_id: null, seller_id: user_id, type: 'sell', shares, price: revenue / shares, total_cost: netRevenue, tax, created_at: new Date().toISOString(), }; trades.push(trade); return { success: true, trade, new_lord_id: newLordId, current_price: newPrices[option_id], }; }; // ==================== 结算 ==================== /** * 结算话题 * @param {string} topicId - 话题ID * @param {string} result - 结果 'yes' | 'no' * @param {string} evidence - 证据说明 * @param {string} settledBy - 裁决者ID * @returns {Object} 结算结果 */ export const settleTopic = (topicId, result, evidence, settledBy) => { const topic = getTopic(topicId); if (!topic) throw new Error('话题不存在'); if (topic.status === 'settled') throw new Error('话题已结算'); // 只有作者可以结算 if (topic.author_id !== settledBy) throw new Error('无权结算'); // 获取获胜方和失败方 const winningOption = result; const losingOption = result === 'yes' ? 'no' : 'yes'; const winners = Array.from(positions.values()).filter( (p) => p.topic_id === topicId && p.option_id === winningOption ); const losers = Array.from(positions.values()).filter( (p) => p.topic_id === topicId && p.option_id === losingOption ); // 分配奖池 if (winners.length === 0) { // 无人获胜,奖池返还给作者 addCredits(topic.author_id, topic.total_pool, '话题奖池返还'); } else { // 计算获胜方总份额 const totalWinningShares = winners.reduce((sum, p) => sum + p.shares, 0); // 按份额分配 winners.forEach((position) => { const share = position.shares / totalWinningShares; const reward = Math.floor(topic.total_pool * share); // 返还本金 + 奖池分成 const refund = Math.floor(position.avg_cost * position.shares); const total = refund + reward; addCredits(position.holder_id, total, `预测获胜 - ${topic.title}`); // 记录胜利 recordWin(position.holder_id, reward); // 删除席位 positions.delete(position.id); removePosition(position.holder_id, position.id); }); } // 失败方损失本金 losers.forEach((position) => { const loss = Math.floor(position.avg_cost * position.shares); // 记录失败 recordLoss(position.holder_id, loss); // 删除席位 positions.delete(position.id); removePosition(position.holder_id, position.id); }); // 更新话题状态 updateTopic(topicId, { status: 'settled', settlement: { result, evidence, settled_by: settledBy, settled_at: new Date().toISOString(), }, }); return { success: true, winners_count: winners.length, losers_count: losers.length, total_distributed: topic.total_pool, }; }; // ==================== 数据导出 ==================== export default { MARKET_CONFIG, // 定价算法 calculatePrice, calculateBuyCost, calculateSellRevenue, calculateTax, // 话题管理 createTopic, getTopic, updateTopic, getTopics, // 席位管理 getPosition, // 交易 buyPosition, sellPosition, // 结算 settleTopic, };