Building Advanced Prediction Market Features: Beyond Binary Markets
Implement advanced prediction market features including parimutuel betting, conditional markets, combinatorial predictions, and dynamic market mechanisms.
Binary yes/no markets are just the beginning. Advanced prediction market features unlock new use cases, attract sophisticated traders, and create sustainable competitive advantages. From parimutuel pools to combinatorial markets, conditional predictions to dynamic mechanisms, this guide explores the cutting edge of prediction market design.
Parimutuel Betting Markets
Unlike fixed-odds markets, parimutuel pools all bets and divides among winners:
contract ParimutuelMarket {
struct Outcome {
string name;
uint256 totalStaked;
mapping(address => uint256) userStakes;
}
struct Market {
string question;
uint256 resolutionTime;
Outcome[] outcomes;
uint256 totalPool;
bool resolved;
uint256 winningOutcome;
}
mapping(uint256 => Market) public markets;
uint256 public marketCount;
function createParimutuelMarket(
string memory question,
string[] memory outcomeNames,
uint256 resolutionTime
) external returns (uint256 marketId) {
require(outcomeNames.length >= 2, "Need at least 2 outcomes");
require(outcomeNames.length <= 10, "Maximum 10 outcomes");
marketId = marketCount++;
Market storage market = markets[marketId];
market.question = question;
market.resolutionTime = resolutionTime;
for (uint i = 0; i < outcomeNames.length; i++) {
market.outcomes.push(Outcome({
name: outcomeNames[i],
totalStaked: 0
}));
}
}
function placeBet(uint256 marketId, uint256 outcomeIndex) external payable {
Market storage market = markets[marketId];
require(!market.resolved, "Market resolved");
require(outcomeIndex < market.outcomes.length, "Invalid outcome");
require(msg.value > 0, "Must bet something");
Outcome storage outcome = market.outcomes[outcomeIndex];
outcome.userStakes[msg.sender] += msg.value;
outcome.totalStaked += msg.value;
market.totalPool += msg.value;
emit BetPlaced(marketId, msg.sender, outcomeIndex, msg.value);
}
function resolve(uint256 marketId, uint256 winningOutcome) external onlyOracle {
Market storage market = markets[marketId];
require(!market.resolved, "Already resolved");
require(winningOutcome < market.outcomes.length, "Invalid outcome");
market.resolved = true;
market.winningOutcome = winningOutcome;
emit MarketResolved(marketId, winningOutcome);
}
function claimWinnings(uint256 marketId) external {
Market storage market = markets[marketId];
require(market.resolved, "Not resolved");
Outcome storage winningOutcome = market.outcomes[market.winningOutcome];
uint256 userStake = winningOutcome.userStakes[msg.sender];
require(userStake > 0, "No winning stake");
// Calculate share of pool
uint256 winnings = (userStake * market.totalPool) / winningOutcome.totalStaked;
// Clear stake
winningOutcome.userStakes[msg.sender] = 0;
// Transfer winnings
payable(msg.sender).transfer(winnings);
emit WinningsClaimed(marketId, msg.sender, winnings);
}
function getOdds(uint256 marketId, uint256 outcomeIndex) external view returns (uint256) {
Market storage market = markets[marketId];
if (market.totalPool == 0) return 0;
// Odds = Total Pool / Outcome Stake
// Returns multiplier (e.g., 2.5x means $2.50 return per $1 bet)
return (market.totalPool * 1e18) / market.outcomes[outcomeIndex].totalStaked;
}
}
Advantages:
- No liquidity provider needed
- Natural odds discovery
- No counterparty risk
- Simple for users to understand
Disadvantages:
- Odds change as bets come in
- No early exit (can't sell position)
- Requires many participants for accurate odds
Conditional Markets (If-Then Markets)
Markets that resolve based on conditions:
contract ConditionalMarket {
struct Condition {
string description;
bool resolved;
bool outcome;
address oracle;
}
struct ConditionalMarket {
string question;
uint256[] conditions; // Market only resolves if all conditions met
uint256 resolutionTime;
bool resolved;
uint256 outcome;
}
mapping(uint256 => Condition) public conditions;
mapping(uint256 => ConditionalMarket) public conditionalMarkets;
uint256 public conditionCount;
uint256 public marketCount;
function createCondition(
string memory description,
address oracle
) external returns (uint256 conditionId) {
conditionId = conditionCount++;
conditions[conditionId] = Condition({
description: description,
resolved: false,
outcome: false,
oracle: oracle
});
}
function createConditionalMarket(
string memory question,
uint256[] memory conditionIds,
uint256 resolutionTime
) external returns (uint256 marketId) {
marketId = marketCount++;
conditionalMarkets[marketId] = ConditionalMarket({
question: question,
conditions: conditionIds,
resolutionTime: resolutionTime,
resolved: false,
outcome: 0
});
}
function resolveCondition(uint256 conditionId, bool outcome) external {
Condition storage condition = conditions[conditionId];
require(msg.sender == condition.oracle, "Not oracle");
require(!condition.resolved, "Already resolved");
condition.resolved = true;
condition.outcome = outcome;
emit ConditionResolved(conditionId, outcome);
}
function checkMarketResolvable(uint256 marketId) public view returns (bool) {
ConditionalMarket storage market = conditionalMarkets[marketId];
// All conditions must be resolved and true
for (uint i = 0; i < market.conditions.length; i++) {
Condition storage condition = conditions[market.conditions[i]];
if (!condition.resolved || !condition.outcome) {
return false;
}
}
return true;
}
function resolveMarket(uint256 marketId, uint256 outcome) external onlyOracle {
require(checkMarketResolvable(marketId), "Conditions not met");
ConditionalMarket storage market = conditionalMarkets[marketId];
market.resolved = true;
market.outcome = outcome;
emit ConditionalMarketResolved(marketId, outcome);
}
}
Use Cases:
- "Will Company X IPO IF they raise Series D?"
- "Will Candidate Y win IF they win the primary?"
- "Will Token Z reach $100 IF Bitcoin reaches $100k?"
Combinatorial Markets
Trade on combinations of outcomes:
contract CombinatorialMarket {
struct CombinationMarket {
string[] events;
uint256[] outcomesPerEvent;
mapping(uint256 => uint256) combinationPrices; // encoded combination => price
uint256 totalCombinations;
}
mapping(uint256 => CombinationMarket) public combinatorialMarkets;
function createCombinatorialMarket(
string[] memory events,
uint256[] memory outcomesPerEvent
) external returns (uint256 marketId) {
marketId = marketCount++;
CombinationMarket storage market = combinatorialMarkets[marketId];
market.events = events;
market.outcomesPerEvent = outcomesPerEvent;
// Calculate total combinations
uint256 total = 1;
for (uint i = 0; i < outcomesPerEvent.length; i++) {
total *= outcomesPerEvent[i];
}
market.totalCombinations = total;
}
// Encode combination as single uint256
// For 3 events with 2, 3, 2 outcomes: combination [0,1,0] = 0*6 + 1*2 + 0 = 2
function encodeCombination(uint256[] memory outcomes) public pure returns (uint256) {
uint256 encoded = 0;
uint256 multiplier = 1;
for (uint i = outcomes.length; i > 0; i--) {
encoded += outcomes[i-1] * multiplier;
multiplier *= 10; // Assuming max 10 outcomes per event
}
return encoded;
}
function tradeCombination(
uint256 marketId,
uint256[] memory combination,
uint256 shares
) external {
uint256 encoded = encodeCombination(combination);
CombinationMarket storage market = combinatorialMarkets[marketId];
// Use LMSR or other AMM to price combination
uint256 cost = calculateCombinationCost(marketId, encoded, shares);
collateral.transferFrom(msg.sender, address(this), cost);
market.combinationPrices[encoded] += shares;
_mintCombinationShares(msg.sender, marketId, encoded, shares);
}
function resolveCombinatorial(
uint256 marketId,
uint256[] memory actualOutcomes
) external onlyOracle {
uint256 winningCombination = encodeCombination(actualOutcomes);
_resolveMarket(marketId, winningCombination);
}
}
Use Cases:
- Election outcomes across multiple states
- Sports tournament brackets
- Multi-variable economic predictions
Dynamic Market Mechanisms
Adaptive Liquidity
contract AdaptiveLiquidityMarket {
uint256 public baseLiquidityParameter;
uint256 public targetSlippage; // Target 1% slippage for $1000 trade
function adjustLiquidityParameter(uint256 marketId) external {
uint256 currentSlippage = measureSlippage(marketId, 1000e18);
if (currentSlippage > targetSlippage * 1.2) {
// Increase liquidity
baseLiquidityParameter = baseLiquidityParameter * 12 / 10;
} else if (currentSlippage < targetSlippage * 0.8) {
// Decrease liquidity (more efficient)
baseLiquidityParameter = baseLiquidityParameter * 8 / 10;
}
// Ensure minimum liquidity
if (baseLiquidityParameter < MIN_LIQUIDITY) {
baseLiquidityParameter = MIN_LIQUIDITY;
}
}
function measureSlippage(uint256 marketId, uint256 tradeSize)
internal view returns (uint256) {
uint256 priceBefore = amm.getPrice(marketId);
uint256 priceAfter = amm.getPriceAfterTrade(marketId, tradeSize);
return ((priceAfter - priceBefore) * 10000) / priceBefore;
}
}
Dynamic Fees
contract DynamicFeeMarket {
function calculateDynamicFee(
uint256 marketId,
uint256 tradeSize
) public view returns (uint256) {
uint256 baseFee = 20; // 0.2%
// Higher fees for larger trades (price impact)
uint256 liquidity = amm.getLiquidity(marketId);
uint256 sizeRatio = (tradeSize * 10000) / liquidity;
// Progressive fee: 0.2% + up to 0.8% for very large trades
uint256 sizeFee = sizeRatio / 10; // 0.1% per 10% of liquidity
// Lower fees for high-volume traders
uint256 volumeDiscount = getVolumeDiscount(msg.sender);
uint256 totalFee = baseFee + sizeFee;
return totalFee > volumeDiscount ? totalFee - volumeDiscount : 0;
}
function getVolumeDiscount(address trader) public view returns (uint256) {
uint256 volume30d = getTraderVolume30d(trader);
if (volume30d > 1000000e18) return 15; // 0.15% discount
if (volume30d > 500000e18) return 10; // 0.10% discount
if (volume30d > 100000e18) return 5; // 0.05% discount
return 0;
}
}
Automated Market Maker Enhancements
Concentrated Liquidity for Prediction Markets
contract ConcentratedLiquidityPM {
struct Position {
uint256 marketId;
uint256 lowerPrice;
uint256 upperPrice;
uint128 liquidity;
address owner;
}
mapping(uint256 => Position) public positions;
uint256 public positionCount;
function addConcentratedLiquidity(
uint256 marketId,
uint256 lowerPrice,
uint256 upperPrice,
uint256 amount0,
uint256 amount1
) external returns (uint256 positionId) {
require(upperPrice > lowerPrice, "Invalid range");
require(upperPrice <= 1e18, "Above max price");
// Calculate liquidity amount
uint128 liquidity = calculateLiquidity(amount0, amount1, lowerPrice, upperPrice);
positionId = positionCount++;
positions[positionId] = Position({
marketId: marketId,
lowerPrice: lowerPrice,
upperPrice: upperPrice,
liquidity: liquidity,
owner: msg.sender
});
// Transfer tokens
collateral.transferFrom(msg.sender, address(this), amount0 + amount1);
emit LiquidityAdded(positionId, msg.sender, lowerPrice, upperPrice, liquidity);
}
function calculateLiquidity(
uint256 amount0,
uint256 amount1,
uint256 lowerPrice,
uint256 upperPrice
) internal pure returns (uint128) {
// L = amount0 * amount1 / (sqrt(upper) - sqrt(lower))
uint256 sqrtLower = sqrt(lowerPrice);
uint256 sqrtUpper = sqrt(upperPrice);
return uint128((amount0 * amount1) / (sqrtUpper - sqrtLower));
}
function collectFees(uint256 positionId) external {
Position storage position = positions[positionId];
require(position.owner == msg.sender, "Not owner");
uint256 feesOwed = calculateUnclaimedFees(positionId);
// Reset fee tracking
_resetFeeTracking(positionId);
// Transfer fees
collateral.transfer(msg.sender, feesOwed);
}
}
Advanced Order Types
Limit Orders
contract LimitOrderBook {
struct LimitOrder {
address trader;
uint256 marketId;
bool isYes;
uint256 price;
uint256 amount;
uint256 filled;
uint256 expiry;
bool active;
}
mapping(uint256 => LimitOrder) public orders;
mapping(uint256 => mapping(uint256 => uint256[])) public priceLevels; // market => price => orderIds
uint256 public orderCount;
function placeLimitOrder(
uint256 marketId,
bool isYes,
uint256 price,
uint256 amount,
uint256 expiry
) external returns (uint256 orderId) {
require(price > 0 && price < 1e18, "Invalid price");
require(amount > 0, "Invalid amount");
require(expiry > block.timestamp, "Invalid expiry");
orderId = orderCount++;
orders[orderId] = LimitOrder({
trader: msg.sender,
marketId: marketId,
isYes: isYes,
price: price,
amount: amount,
filled: 0,
expiry: expiry,
active: true
});
priceLevels[marketId][price].push(orderId);
// Lock collateral
collateral.transferFrom(msg.sender, address(this), amount);
emit LimitOrderPlaced(orderId, msg.sender, marketId, isYes, price, amount);
}
function cancelOrder(uint256 orderId) external {
LimitOrder storage order = orders[orderId];
require(order.trader == msg.sender, "Not owner");
require(order.active, "Not active");
order.active = false;
// Return unfilled amount
uint256 unfilled = order.amount - order.filled;
collateral.transfer(msg.sender, unfilled);
emit LimitOrderCancelled(orderId);
}
function matchOrders(uint256 marketId) external {
uint256 bestBid = getBestBid(marketId);
uint256 bestAsk = getBestAsk(marketId);
// Match while bid >= ask
while (bestBid >= bestAsk && bestBid > 0 && bestAsk < 1e18) {
uint256 matched = _matchBestOrders(marketId, bestBid, bestAsk);
if (matched == 0) break;
bestBid = getBestBid(marketId);
bestAsk = getBestAsk(marketId);
}
}
}
Stop-Loss and Take-Profit
contract ConditionalOrders {
struct ConditionalOrder {
address trader;
uint256 marketId;
uint256 triggerPrice;
uint256 limitPrice;
uint256 amount;
bool isStopLoss; // true = stop-loss, false = take-profit
bool isBuy;
bool triggered;
}
mapping(uint256 => ConditionalOrder) public conditionalOrders;
uint256 public conditionalOrderCount;
function placeStopLoss(
uint256 marketId,
uint256 stopPrice,
uint256 amount
) external returns (uint256 orderId) {
orderId = conditionalOrderCount++;
conditionalOrders[orderId] = ConditionalOrder({
trader: msg.sender,
marketId: marketId,
triggerPrice: stopPrice,
limitPrice: 0, // Market order
amount: amount,
isStopLoss: true,
isBuy: false,
triggered: false
});
emit StopLossPlaced(orderId, msg.sender, marketId, stopPrice, amount);
}
function checkAndExecute(uint256 orderId) external {
ConditionalOrder storage order = conditionalOrders[orderId];
require(!order.triggered, "Already triggered");
uint256 currentPrice = amm.getPrice(order.marketId);
bool shouldTrigger = order.isStopLoss
? currentPrice <= order.triggerPrice // Stop-loss: price drops to trigger
: currentPrice >= order.triggerPrice; // Take-profit: price rises to trigger
if (shouldTrigger) {
order.triggered = true;
_executeConditionalOrder(orderId);
}
}
}
Market Creation Tools
AI-Assisted Market Creation
contract AIMarketAssistant {
struct MarketSuggestion {
string question;
string category;
uint256 suggestedResolutionTime;
uint256 confidenceScore;
string[] similarMarkets;
}
function suggestMarketParameters(
string memory topic
) external view returns (MarketSuggestion memory) {
// This would integrate with off-chain AI
// For now, example structure:
return MarketSuggestion({
question: generateQuestion(topic),
category: categorizeTopic(topic),
suggestedResolutionTime: suggestResolutionTime(topic),
confidenceScore: calculateConfidence(topic),
similarMarkets: findSimilarMarkets(topic)
});
}
function validateQuestionQuality(string memory question)
external pure returns (uint256 score) {
// Check clarity
uint256 clarityScore = checkClarity(question);
// Check specificity
uint256 specificityScore = checkSpecificity(question);
// Check resolvability
uint256 resolvabilityScore = checkResolvability(question);
return (clarityScore + specificityScore + resolvabilityScore) / 3;
}
}
Template Markets
contract MarketTemplates {
struct Template {
string name;
string questionFormat;
string[] parameterNames;
uint256[] defaultValues;
string category;
uint256 defaultResolutionDays;
}
mapping(uint256 => Template) public templates;
uint256 public templateCount;
function createTemplateMarket(
uint256 templateId,
string[] memory parameters
) external returns (uint256 marketId) {
Template storage template = templates[templateId];
// Replace placeholders in question format
string memory question = template.questionFormat;
for (uint i = 0; i < parameters.length; i++) {
string memory placeholder = string(abi.encodePacked("{", uint2str(i), "}"));
question = replace(question, placeholder, parameters[i]);
}
uint256 resolutionTime = block.timestamp + (template.defaultResolutionDays * 1 days);
marketId = _createMarket(question, template.category, resolutionTime);
}
}
Social Features
Copy Trading
contract CopyTrading {
struct TraderProfile {
address trader;
uint256 totalPnL;
uint256 winRate;
uint256 followers;
uint256 feeRate;
bool isPublic;
}
struct CopySettings {
address leader;
uint256 maxTradeSize;
uint256 allocationPercentage; // Percentage of follower's capital
bool active;
}
mapping(address => TraderProfile) public traderProfiles;
mapping(address => CopySettings) public copySettings;
mapping(address => address[]) public followerLists;
function becomePublicTrader(uint256 feeRate) external {
require(feeRate <= 1000, "Max 10% fee"); // 10% of profits
traderProfiles[msg.sender] = TraderProfile({
trader: msg.sender,
totalPnL: 0,
winRate: 0,
followers: 0,
feeRate: feeRate,
isPublic: true
});
}
function followTrader(address leader, uint256 allocationPercentage) external {
require(traderProfiles[leader].isPublic, "Trader not public");
require(allocationPercentage <= 5000, "Max 50% allocation"); // Risk management
copySettings[msg.sender] = CopySettings({
leader: leader,
maxTradeSize: type(uint256).max,
allocationPercentage: allocationPercentage,
active: true
});
followerLists[leader].push(msg.sender);
traderProfiles[leader].followers++;
emit StartedFollowing(msg.sender, leader, allocationPercentage);
}
function executeCopyTrade(
address leader,
uint256 marketId,
bool isYes,
uint256 leaderAmount
) external {
CopySettings storage settings = copySettings[msg.sender];
require(settings.leader == leader, "Not following this trader");
require(settings.active, "Copy trading inactive");
// Calculate follower trade size
uint256 followerBalance = getBalance(msg.sender);
uint256 tradeSize = (followerBalance * settings.allocationPercentage) / 10000;
// Scale based on leader's position size relative to their balance
uint256 leaderBalance = getBalance(leader);
uint256 leaderPositionSize = (leaderAmount * 10000) / leaderBalance;
tradeSize = (tradeSize * leaderPositionSize) / 10000;
// Cap at max trade size
if (tradeSize > settings.maxTradeSize) {
tradeSize = settings.maxTradeSize;
}
// Execute copy trade
_executeTrade(msg.sender, marketId, isYes, tradeSize);
emit CopyTradeExecuted(msg.sender, leader, marketId, tradeSize);
}
}
Social Trading Feed
interface SocialActivity {
type: 'trade' | 'market_created' | 'position_closed' | 'achievement';
user: string;
timestamp: number;
marketId?: string;
details: {
side?: 'yes' | 'no';
amount?: number;
pnl?: number;
achievement?: string;
};
}
class SocialFeed {
async getFeed(userAddress: string): Promise<SocialActivity[]> {
// Get followed traders
const followedTraders = await this.getFollowedTraders(userAddress);
// Get recent activity from followed traders
const activities = await db.query(`
SELECT * FROM social_activity
WHERE user_address = ANY($1)
AND timestamp > $2
ORDER BY timestamp DESC
LIMIT 50
`, [followedTraders, Date.now() - 7 * 24 * 60 * 60 * 1000]);
return activities;
}
}
Conclusion
Advanced prediction market features differentiate mature platforms from basic implementations. The most successful platforms combine:
- Multiple Market Types: Binary, scalar, categorical, combinatorial
- Sophisticated Trading: Limit orders, stop-losses, conditional orders
- Dynamic Mechanisms: Adaptive liquidity, dynamic fees
- Social Features: Copy trading, leaderboards, social feeds
- Advanced AMMs: Concentrated liquidity, multi-asset pools
- User Experience: AI-assisted market creation, templates
These features attract sophisticated traders, increase engagement, and create network effects that protect against competition. However, they also increase complexity—implement carefully, test thoroughly, and always prioritize user safety and regulatory compliance.
The future of prediction markets lies not in basic yes/no questions, but in sophisticated financial instruments that combine the best of DeFi, information markets, and social trading.