Price Manipulation Example

Our goal in this example is to develop an agent based model of price manipulation in a prediction market as described in the arXiv paper entitled How manipulable are prediction markets? (Rasooly & Rozzi, 2025). In this example, a small set of agents trade shares in a prediction market based on a constant product market maker. During the trading period, a manipulator increases the current market price by .05 units. The model illustrates partial recovery of the market price, similar to what was found empirically in a large scale field experiment conducted by the authors (Rasooly & Rozzi, 2025).

If you prefer to skip the explanation below, click on the ▶ icon to reveal a full version of the code.

Full Code
using ABMPredictionMarkets
using ABMPredictionMarkets: compute_optimal_purchase
using ABMPredictionMarkets: compute_price
using ABMPredictionMarkets: cost_to_shares
using ABMPredictionMarkets: get_market
using ABMPredictionMarkets: init
using ABMPredictionMarkets: price_to_cost
using ABMPredictionMarkets: shares_to_cost
using ABMPredictionMarkets: update_reserves!
using ABMPredictionMarkets: transact!
using Agents
using Distributions
using Plots
using Random
import ABMPredictionMarkets: agent_step!

@agent struct CPMMAgent(NoSpaceAgent) <: MarketAgent
    judgments::Vector{Vector{Float64}}
    money::Float64
    shares::Vector{Vector{Float64}}
    λ::Float64
end

function model_step!(model)
    agent = random_agent(model)
    agent_step!(agent, model)
    if abmtime(model) ∈ model.times
        market = get_market(model)
        current_price = compute_price(market, 1, true)
        target_price = min(1, current_price + 0.05)
        cost = price_to_cost(market, target_price, 1, true)
        n_shares = cost_to_shares(market, cost, 1, true)
        update_reserves!(market, n_shares, cost, 1, true)
        push!(model.trade_volume[1], n_shares)
        push!(model.market_prices[1], compute_price(market, 1, true))
    end
    return nothing
end

function agent_step!(agent, ::CPMMAgent, market::AbstractCPMM, model)
    (; judgments, λ) = agent
    if agent.shares[1][1] > 0
        # sell yes shares
        n_shares = -agent.shares[1][1]
        cost = shares_to_cost(market, n_shares, 1, true)
        order = AMMOrder(; id = agent.id, option = true, cost, n_shares)
        transact!(order, market, model, 1)
        pop!(model.trade_volume[1])
        pop!(model.market_prices[1])
    end
    if agent.shares[1][2] > 0
        # sell no shares
        n_shares = -agent.shares[1][2]
        cost = shares_to_cost(market, n_shares, 1, false)
        order = AMMOrder(; id = agent.id, option = false, cost, n_shares)
        transact!(order, market, model, 1)
        pop!(model.trade_volume[1])
        pop!(model.market_prices[1])
    end

    price = compute_price(market, 1, true)
    belief = (1 - λ) * judgments[1][1] + λ * price
    if belief ≥ price
        # buy yes shares
        cost = compute_optimal_purchase(agent, market, belief, 1, true)
        n_shares = cost_to_shares(market, cost, 1, true)
        order = AMMOrder(; id = agent.id, option = true, cost, n_shares)
        transact!(order, market, model, 1)
    elseif belief < price
        # buy no shares
        cost = compute_optimal_purchase(agent, market, belief, 1, false)
        n_shares = cost_to_shares(market, cost, 1, false)
        order = AMMOrder(; id = agent.id, option = false, cost, n_shares)
        transact!(order, market, model, 1)
    end
    return nothing
end

function initialize(
    agent_type::Type{<:CPMMAgent};
    n_agents,
    λ,
    money,
    yes_reserves,
    no_reserves,
    manipulate_time
)
    yes_reserves = deepcopy(yes_reserves)
    no_reserves = deepcopy(no_reserves)
    space = nothing
    model = StandardABM(
        agent_type,
        space;
        properties = CPMM(; yes_reserves, no_reserves, times = [manipulate_time]),
        model_step!,
        scheduler = Schedulers.Randomly()
    )
    for i ∈ 1:n_agents
        p = (i - 1) / (n_agents - 1)
        judgments = [[p, 1-p]]
        add_agent!(
            model;
            judgments,
            money,
            λ,
            shares = [zeros(2)]
        )
    end
    return model
end

config = (
    n_agents = 11,
    λ = 0.0,
    money = 100,
    no_reserves = [1000.0],
    yes_reserves = [1000.0],
    manipulate_time = 100,
)

market_prices = map(1:1000) do _
    model = initialize(CPMMAgent; config...)
    run!(model, 200)
    model.market_prices[1]
end

plot(
    mean(market_prices),
    ylims = (.4, .6),
    xlabel = "Day",
    ylabel = "Price of Yes Share",
    linewidth = 2.5,
    grid = false,
    leg = false,
)
hline!([.5], color = :black, linestyle = :dash)

Load Dependencies

Our first step is to load the required dependencies for simulation and plotting. In addition, we will import the function agent_step! so we can create a new method defining the behavior of the agents in the price manipulation simulation.

using ABMPredictionMarkets
using ABMPredictionMarkets: compute_optimal_purchase
using ABMPredictionMarkets: compute_price
using ABMPredictionMarkets: cost_to_shares
using ABMPredictionMarkets: get_market
using ABMPredictionMarkets: init
using ABMPredictionMarkets: price_to_cost
using ABMPredictionMarkets: shares_to_cost
using ABMPredictionMarkets: update_reserves!
using ABMPredictionMarkets: transact!
using Agents
using Distributions
using Plots
using Random
import ABMPredictionMarkets: agent_step!

CPMM Agent

The code block below defines an agent type called CPMMAgent. In this agent type, the first three fields are required, and the fourth field is an optional parameter included for this specific simulation scenario. The fields are defined as follows:

  • judgements: a vector of vectors in which elements of the outer vector represent individual prediction markets and elements of the inner vectors represent subjective price estimates of options of a given market. For example judgements = [[.3,.7],[.2,.8]] contains price estimates for two prediction markets and [.3,.7] represents the price estimates of the first and second options of the first prediction market. Currently, CPMM only supports binary prediction markets, but multiple prediction markets may be used within the same simulation.
  • money: the amount of money in dollars available to purchase shares.
  • shares: a vector of vectors recording the number of shares the agent owns. Similar to the field judgments elements of the outer vector represent individual prediction markets and elements of inner vector represent different options within a prediction market.
  • λ: the weight given to an estimated price in judgments relative to the current market place, such that $\lambda \in \left[0, 1 \right]$.
@agent struct CPMMAgent(NoSpaceAgent) <: MarketAgent
    judgments::Vector{Vector{Float64}}
    money::Float64
    shares::Vector{Vector{Float64}}
    λ::Float64
end

Constant Product Market Maker

The price manipulation simulation uses a type of prediction market called a constant product market maker (CPMM). A CPMM is a type of automated market maker which uses an algorithm ensure liquidity in a market and adjust the price of an asset based on demand. The price of a share in a CPMM is determined by the product of the amount of reserves for yes shares and no shares:

\[r_y \cdot r_n = k,\]

where $r_y$ and $r_n$ are the reserves for yes and no shares, respectively. Share prices are constrained such that the product of yes and no reserves must equal the constant $k$, as shown below:

Plot Code
reserve_plot = let
    x = .01:.01:250
    y = 1000 ./ x
    plot(x, y, xlabel = "Yes Reserves", ylabel = "No Reserves", lims = (0, 250), grid = false, leg = false)
end
nothing
reserve_plot
Example block output

The price is computed as the ratio of reserves. For example, the price for a yes share is $\frac{r_n}{r_y + r_n}$.

Model Step Function

On each iteration of the simulation, the function model_step! is called, which performs the following actions:

  1. Select a random agent and trade via the agent_step! function.
  2. Manipulate the current price by $.05$ units at specified iterations.
function model_step!(model)
    agent = random_agent(model)
    agent_step!(agent, model)
    if abmtime(model) ∈ model.times
        market = get_market(model)
        current_price = compute_price(market, 1, true)
        target_price = min(1, current_price + 0.05)
        cost = price_to_cost(market, target_price, 1, true)
        n_shares = cost_to_shares(market, cost, 1, true)
        update_reserves!(market, n_shares, cost, 1, true)
        push!(model.trade_volume[1], n_shares)
        push!(model.market_prices[1], compute_price(market, 1, true))
    end
    return nothing
end
model_step! (generic function with 1 method)

Agent Step Function

The agent_step!, which is called in the model_step! function above, defines the trading behavior of the agents. agent_step! performs the following actions:

  1. sell any available yes or no shares
  2. define belief $b$ for price of yes shares as $b = \lambda \cdot p + (1 - \lambda) \cdot j$, where $p$ is the current price of a yes share and $j$ is the estimated price. Buy yes if $b \leq j$, buy no otherwise.
function agent_step!(agent, ::CPMMAgent, market::AbstractCPMM, model)
    (; judgments, λ) = agent
    if agent.shares[1][1] > 0
        # sell yes shares
        n_shares = -agent.shares[1][1]
        cost = shares_to_cost(market, n_shares, 1, true)
        order = AMMOrder(; id = agent.id, option = true, cost, n_shares)
        transact!(order, market, model, 1)
        pop!(model.trade_volume[1])
        pop!(model.market_prices[1])
    end
    if agent.shares[1][2] > 0
        # sell no shares
        n_shares = -agent.shares[1][2]
        cost = shares_to_cost(market, n_shares, 1, false)
        order = AMMOrder(; id = agent.id, option = false, cost, n_shares)
        transact!(order, market, model, 1)
        pop!(model.trade_volume[1])
        pop!(model.market_prices[1])
    end

    price = compute_price(market, 1, true)
    belief = (1 - λ) * judgments[1][1] + λ * price
    if belief ≥ price
        # buy yes shares
        cost = compute_optimal_purchase(agent, market, belief, 1, true)
        n_shares = cost_to_shares(market, cost, 1, true)
        order = AMMOrder(; id = agent.id, option = true, cost, n_shares)
        transact!(order, market, model, 1)
    elseif belief < price
        # buy no shares
        cost = compute_optimal_purchase(agent, market, belief, 1, false)
        n_shares = cost_to_shares(market, cost, 1, false)
        order = AMMOrder(; id = agent.id, option = false, cost, n_shares)
        transact!(order, market, model, 1)
    end
    return nothing
end
agent_step! (generic function with 5 methods)

Model Initialization Function

The function specified in the code block below initializes the model using parameter values provided by the user. The model contains agents and a CPMM without a spatial component. The keyword arguments for the function are defined as follows:

  • agent_type: the agent type used in the simlation
  • n_agents: the number of agents in the simulation
  • λ: the price weight parameter
  • money: the initial amount of money available for each agent to buy shares
  • yes_reserves: the number of reserves for yes shares
  • no_reserves: the number of reserves for no shares
  • manipulate_time: the time at which the market is manipulated

The estimated price ranges from 0 to 1 across agents, such that the average estimate is .50.

function initialize(
    agent_type::Type{<:CPMMAgent};
    n_agents,
    λ,
    money,
    yes_reserves,
    no_reserves,
    manipulate_time
)
    yes_reserves = deepcopy(yes_reserves)
    no_reserves = deepcopy(no_reserves)
    space = nothing
    model = StandardABM(
        agent_type,
        space;
        properties = CPMM(; yes_reserves, no_reserves, times = [manipulate_time]),
        model_step!,
        scheduler = Schedulers.Randomly()
    )
    for i ∈ 1:n_agents
        p = (i - 1) / (n_agents - 1)
        judgments = [[p, 1-p]]
        add_agent!(
            model;
            judgments,
            money,
            λ,
            shares = [zeros(2)]
        )
    end
    return model
end
initialize (generic function with 1 method)

Set RNG Seed

In the code block below, we set the seed for the random number generator to ensure reproducible results.

Random.seed!(5471)
Random.TaskLocalRNG()

Configure Model

In the NamedTuple below, we will set the values for the keyword arguments passed to the initialize function. The parameter values were selected to reproduce Figure 1 (a) reported in Rasooly & Rozzi (2025).

config = (
    n_agents = 11,
    λ = 0.0,
    money = 100,
    no_reserves = [1000.0],
    yes_reserves = [1000.0],
    manipulate_time = 100,
)
(n_agents = 11, λ = 0.0, money = 100, no_reserves = [1000.0], yes_reserves = [1000.0], manipulate_time = 100)

Simulate the Model

Now that we have defined the agents and the model parameters, we are in the position to simulate the model. The code below runs the model 1000 times and returns a vector of 200 market prices per run.

market_prices = map(1:1000) do _
    model = initialize(CPMMAgent; config...)
    run!(model, 200)
    model.market_prices[1]
end
1000-element Vector{Vector{Float64}}:
 [0.4524886877828055, 0.4477324694230083, 0.4801745824895639, 0.5194342857208619, 0.5130849450340592, 0.5130849450340594, 0.5110658032884317, 0.5110658032884319, 0.5113656856778853, 0.4826154777755965  …  0.5316737975647181, 0.5316737972396831, 0.5316739070013891, 0.5316738978379532, 0.5316738944834571, 0.531673894483457, 0.5316740687248238, 0.531674055767023, 0.5316740557670229, 0.5316740471219249]
 [0.48169631372795607, 0.48169631372795607, 0.530996129042322, 0.49070158391276747, 0.4907015839127674, 0.5006542531567131, 0.5379727504648673, 0.5347086472493027, 0.5348255123751512, 0.5348255123751512  …  0.528237922666021, 0.5282396374034545, 0.5282395098954297, 0.5281589408134781, 0.5281660381277518, 0.5281660381277518, 0.5281660381277516, 0.5281685768690797, 0.5281685768690796, 0.5281677357658785]
 [0.5, 0.5475113122171946, 0.5249161733187774, 0.5249161733187773, 0.5135494563573167, 0.4748788455949926, 0.4954753186509275, 0.4954753186509276, 0.49778163311864626, 0.4991665863794336  …  0.5278750612235928, 0.5278740832360249, 0.5278732158987136, 0.5278731988242804, 0.5278839955836218, 0.5278839955836219, 0.527883995583622, 0.5278524376651096, 0.5278540761949186, 0.5278573467013318]
 [0.5091059865213138, 0.5082782943802381, 0.47010650292592665, 0.47357192852460434, 0.5134454803317636, 0.5031162407298684, 0.5211325497280591, 0.4914827690768073, 0.4448209137719316, 0.45075917663534343  …  0.53293760272394, 0.5329448530532852, 0.5334079373313944, 0.5334079373313946, 0.5333790879687986, 0.5333790879687986, 0.5333790879687987, 0.5333790879687985, 0.5333790879687984, 0.533362492195294]
 [0.4908940134786863, 0.5194346858453109, 0.4700261154747992, 0.4718536638679071, 0.4744087326245876, 0.5142048320031039, 0.5145246130818829, 0.5223031378738423, 0.5218307525445178, 0.521557750732004  …  0.5302674329132518, 0.5302679904343344, 0.5302682025789558, 0.5302682025789558, 0.5302692034817409, 0.5302692034817409, 0.5302691588628919, 0.5302691588628919, 0.5302691588628918, 0.5302691048333128]
 [0.5373813247573764, 0.5156844115502469, 0.5051521737112721, 0.532359567695283, 0.5765916632041205, 0.5739175451808225, 0.5739175451808226, 0.519551481752639, 0.5268696571977658, 0.5299230377558668  …  0.5223878159323431, 0.522386877574349, 0.5223867507810828, 0.5223867507810829, 0.5223865786666416, 0.5223866220433766, 0.5223866297546913, 0.5223866297546913, 0.5223866297546913, 0.5223866252785981]
 [0.5475113122171946, 0.51548754491108, 0.5140802004908667, 0.5404437952161055, 0.49928862583086847, 0.5018156467797785, 0.5018156467797784, 0.5390224917191854, 0.5390224917191854, 0.5390224917191854  …  0.5242282475795906, 0.5242287536867514, 0.5242287096827543, 0.5242286940659378, 0.5242286954237575, 0.5242286954237575, 0.5242286954237575, 0.5242301710708439, 0.5242303712553851, 0.5242302982382543]
 [0.4524886877828055, 0.41978505357545387, 0.4746841685785368, 0.458729318914882, 0.458729318914882, 0.45872931891488206, 0.4624695565229188, 0.46246955652291877, 0.4383493223925034, 0.4376784881112421  …  0.5403071289265566, 0.5403071289265567, 0.5403072012151351, 0.5403085459154937, 0.5403085459154936, 0.5403084925834494, 0.5403084925834494, 0.5403084177945855, 0.5403096770616633, 0.5403100545097024]
 [0.5183036862720438, 0.4791863274852672, 0.4534631622632677, 0.5054105324588479, 0.5022153127437995, 0.5020139224731988, 0.5392016900325328, 0.5392016900325328, 0.5370580846660965, 0.5355985044166366  …  0.5296520764231997, 0.5296519994655922, 0.5296519994655922, 0.529652082273576, 0.5296520773041935, 0.5296520773041936, 0.5296520773041937, 0.529652077756888, 0.5296521494584956, 0.5296521494584956]
 [0.5373813247573764, 0.5248954342025507, 0.5043193470595487, 0.4762241773992389, 0.4311089999934946, 0.4005707490451751, 0.43696547790899193, 0.4903892435487358, 0.49038924354873586, 0.4920772279810555  …  0.5391488739608664, 0.5391488739608664, 0.5391488783161851, 0.5391489209964528, 0.5391489209964528, 0.5391486139926185, 0.5391486139926186, 0.5391485581513438, 0.539148583683528, 0.5391485836835281]
 ⋮
 [0.4908940134786863, 0.5291443685161761, 0.5355834030099502, 0.48464600446916223, 0.48519397853958346, 0.5048545927223573, 0.4670086944078624, 0.4699598625652007, 0.45444631000078817, 0.45562668094550507  …  0.5330276751450491, 0.5330258645983257, 0.5330259303583601, 0.5330259303583607, 0.5330252906520481, 0.5330252906520481, 0.5330277624261447, 0.5330276958846707, 0.533026530384266, 0.5330264171603153]
 [0.4723086923163467, 0.47230869231634665, 0.47230869231634665, 0.43762764809146, 0.46152004983366635, 0.46217816036795495, 0.5133237948163745, 0.4645053394844422, 0.44950213487421875, 0.45028373187292414  …  0.5346603180239303, 0.5346601659589983, 0.5346601659589983, 0.5346598655554052, 0.5346598818182915, 0.5347390930512965, 0.5347362105866295, 0.5347306203816274, 0.5347306203816274, 0.5347306203816274]
 [0.5, 0.4524886877828055, 0.41978505357545387, 0.45461570128009055, 0.4962152725060373, 0.49363824615178203, 0.5125267677599482, 0.5587852023131595, 0.5565567204256214, 0.5515919164031886  …  0.5331906340532723, 0.5331906320617575, 0.5331906320617575, 0.5331906256904698, 0.533190617895987, 0.5331903655642517, 0.5331897501448761, 0.5331898263423437, 0.5331898976648901, 0.5331896097963811]
 [0.5475113122171946, 0.5706671048908782, 0.5733106738742455, 0.566711610155627, 0.5667116101556271, 0.5667116101556271, 0.5667116101556271, 0.5787843587792912, 0.5776805450449828, 0.5776805450449827  …  0.5282103652173509, 0.5282103541917875, 0.5282282492398469, 0.5282276516357839, 0.5282260478766625, 0.528224955827233, 0.5282241129359484, 0.5282241772769105, 0.5282243072292742, 0.5282242952471584]
 [0.5475113122171946, 0.5432098765432098, 0.5432098765432098, 0.5115671570282686, 0.4730837108218285, 0.49384215589405894, 0.4949283030852148, 0.49934883927132656, 0.4984067567197429, 0.49840675671974294  …  0.5321893128036356, 0.532189068436679, 0.5321823522306252, 0.5321829547838802, 0.532184056567894, 0.5321839936966328, 0.5321839936966328, 0.5321843746104635, 0.532184671211918, 0.5321846712119179]
 [0.48169631372795607, 0.5110865896452361, 0.5283654359604059, 0.5348758717848271, 0.5226141511205191, 0.4928288090204717, 0.4939459577231526, 0.4939459577231525, 0.5319061956316941, 0.5321672026201308  …  0.5254130844252116, 0.5254130829111425, 0.5254130696872485, 0.5254130830673148, 0.5254130821789543, 0.5254130821789543, 0.5254130822105543, 0.5254130822445795, 0.5254130823209969, 0.5254130823209968]
 [0.5373813247573764, 0.506258874114378, 0.4581301264974426, 0.4710165478939989, 0.4731694101220207, 0.5033391800831654, 0.5014137588187402, 0.49217885297191083, 0.49288978725432886, 0.49288978725432886  …  0.531135372667687, 0.5311353726676871, 0.5311358481536748, 0.5311349533293772, 0.5311339958271156, 0.5311339958271156, 0.531134108025533, 0.5311343446024892, 0.5311343253239108, 0.5311343253239107]
 [0.48169631372795607, 0.4460909016015294, 0.40411251335217163, 0.4602970807001579, 0.46196174340312385, 0.5028991066556561, 0.530318402413668, 0.4998322015697879, 0.4998322015697878, 0.4999422446078429  …  0.5400576173971994, 0.5400576173971994, 0.5400576173971994, 0.5400556239213479, 0.5400557798687017, 0.5400520747432955, 0.5400520747432955, 0.5400333644203028, 0.5400348499154196, 0.5400365667557646]
 [0.5373813247573764, 0.48627659691121494, 0.48627659691121494, 0.48627659691121494, 0.4881121805193299, 0.5367910877356429, 0.5609825650861375, 0.5609825650861375, 0.564507812026097, 0.5767876222315275  …  0.5242116062812653, 0.5242135446738637, 0.5242092641922909, 0.5242100610582484, 0.5242106915651817, 0.5242107701838414, 0.5242107701838414, 0.5242107701838415, 0.5242107616631527, 0.5242106304297883]

Plot the Results

The code block below plots the price as a function of time averaged across the 1000 simulations. The dashed line denotes the expected price based on the beliefs of the agents. At day 100, a manipulator inflates the price by 5 cents. The true price shows a gradual, but partial recovery, which suggests that price manipulation can have an enduring effect.

plot(
    mean(market_prices),
    ylims = (.39, .61),
    xlabel = "Day",
    ylabel = "Price of Yes Share",
    linewidth = 2.5,
    grid = false,
    leg = false,
)
hline!([.5], color = :black, linestyle = :dash)
Example block output

References

Rasooly, I., & Rozzi, R. (2025). How manipulable are prediction markets?. arXiv preprint arXiv:2503.03312.