Building Advanced Prediction Market Features: Beyond Binary Markets

Implement advanced prediction market features including parimutuel betting, conditional markets, combinatorial predictions, and dynamic market mechanisms.

Kevan Shah · · 16 min read

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:

  1. Multiple Market Types: Binary, scalar, categorical, combinatorial
  2. Sophisticated Trading: Limit orders, stop-losses, conditional orders
  3. Dynamic Mechanisms: Adaptive liquidity, dynamic fees
  4. Social Features: Copy trading, leaderboards, social feeds
  5. Advanced AMMs: Concentrated liquidity, multi-asset pools
  6. 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.