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 examplejudgements = [[.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 fieldjudgments
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 injudgments
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
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:
- Select a random agent and trade via the
agent_step!
function. - 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:
- sell any available yes or no shares
- 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 simlationn_agents
: the number of agents in the simulationλ
: the price weight parametermoney
: the initial amount of money available for each agent to buy sharesyes_reserves
: the number of reserves for yes sharesno_reserves
: the number of reserves for no sharesmanipulate_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)
References
Rasooly, I., & Rozzi, R. (2025). How manipulable are prediction markets?. arXiv preprint arXiv:2503.03312.