Files
vf_react/src/services/predictionMarketService.js
2025-11-23 18:11:48 +08:00

739 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 预测市场服务
* 核心功能:话题管理、席位交易、动态定价、领主系统、奖池分配
*/
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,
};