Building an Artificial Market From First Principles
I started with zero market microstructure knowledge, a blank Python file, and one question: can a market emerge from simple interactions?`
A few weeks ago I started a project that sounded deceptively simple.
Build a market.
Not a trading strategy.
Not a backtester.
Not a machine learning model.
A market.
The goal was not realism.
At least not initially.
The goal was understanding.
I wanted to see whether a believable market could emerge from a collection of simple agents interacting with one another.
Before reading papers.
Before studying existing market simulators.
Before importing sophisticated frameworks.
I wanted to start from almost no knowledge and force myself to make mistakes.
The reasoning was simple:
If I copied existing implementations too early, I would inherit their assumptions before understanding why those assumptions existed.
So I started with a blank file and a question:
How does a price move when nobody is explicitly telling it where to go?
That question turned out to be much harder than I expected.
The Smallest Possible Market
The first version of the simulator contained only two components:
Agents
Market
The market had no order book.
No bid-ask spread.
No matching engine.
No transaction costs.
No leverage.
No external information.
Every simplification was intentional.
The objective was not realism.
The objective was to identify the smallest set of mechanisms required for prices to emerge from interaction.
The architecture looked like this:
Agents ↓ Actions ↓ Market Aggregation ↓ Price Update ↓ New Agent Decisions
This feedback loop became the foundation of everything that followed.
Even today, every additional feature still plugs into this same structure.
The First Agent
The first agent knew almost nothing.
It observed:
current price
recent price movement
and maintained:
cash
holdings
risk tolerance
a simple profit-and-loss memory
The decision process was intentionally minimal.
First the agent generated a tendency value:
tendency = price movement + pnl effect + noise
This tendency was transformed into a probability distribution:
positive tendency → buy bias
negative tendency → sell bias
small tendency → hold bias
The result was not intended to model actual traders.
Instead it represented a behavioral particle.
A tiny unit capable of reacting to market information.
One of the first surprises was that even these extremely primitive agents produced noticeably different market trajectories across simulation runs.
The system already exhibited path dependence.
Small random differences early in the simulation frequently propagated into very different outcomes later.
buy_prob = max(0, tendency)
sell_prob = max(0, -tendency)
hold_prob = 1 - buy_prob - sell_prob
Defining Price
The biggest design problem was not agents.
It was pricing.
Once agents decide to buy or sell, how should the market respond?
Initially I considered several possibilities:
random price updates
direct demand minus supply
volume-weighted mechanisms
explicit order books
The challenge was finding something simple enough to reason about while still allowing interaction to matter.
Eventually I settled on a quantity I called imbalance:
imbalance = (buy pressure - sell pressure) / (buy pressure + sell pressure)
The normalization was important.
Without it, market size and market direction become entangled.
For example:
5 buys vs 10 sells
and
5000 buys vs 5005 sells
produce very different interpretations despite having similar raw differences.
The imbalance formulation isolates directional pressure and bounds the result between -1 and 1.
Once imbalance existed, price updates became straightforward:
returns = imbalance × sensitivity
price = price × (1 + returns)
This was the first version of the pricing engine.
Simple.
Possibly wrong.
But simple enough to study.
$$I=B+S/ B−S$$
The Hidden Assumptions
- Assumption 1: One global price
- Assumption 2: No liquidity
- Assumption 3: No transaction costs
- Assumption 4: No leverage
- Assumption 5: Agents only see local information
The most valuable thing I learned during this phase wasn't how to write the simulator.
It was learning to identify assumptions that were invisible when I first wrote the code.
By the end of the first phase, the simulator was functioning.
Prices moved.
Agents traded.
Different runs produced different trajectories.
But something still felt wrong.
Every agent behaved almost identically.
They had different risk tolerances, but they shared the same worldview.
The market had participants.
It did not yet have personalities.
That observation led directly to the next phase of the project:
behavioral heterogeneity.
The question was no longer:
Can agents move prices?
The question became:
What happens when different kinds of traders disagree about reality?
The First Behavioral Agent Broke My Market Simulator
In the previous phase, I built the smallest market I could think of.
Agents generated actions.
The market aggregated buy and sell pressure.
Price moved according to aggregate imbalance.
The system worked.
At least, it appeared to.
Prices moved.
Different runs produced different trajectories.
The feedback loop existed:
Agents → Actions → Imbalance → Price → New Decisions
But there was still a problem.
Every agent interpreted the market in exactly the same way.
They had different cash balances.
Different holdings.
Different risk tolerances.
But they shared the same worldview.
The market had participants.
It did not yet have personalities.
That became the goal of the next phase.
Introduce behavioral heterogeneity and see whether local decision differences could generate different market dynamics.
The First Real Agent Personality
The first behavioral subtype I introduced was a Momentum Trader.
The idea was simple.
Normal agents reacted primarily to recent price movement and a small amount of profit-and-loss feedback.
Momentum traders would go one step further.
Instead of reacting only to the most recent outcome, they would remember a history of outcomes and reinforce trends that appeared to be working.
The implementation was surprisingly small.
Rather than rewriting the market itself, I only needed to override a single method:
tendency()
The market remained unchanged.
The pricing mechanism remained unchanged.
Only the interpretation of information changed.
This was intentional.
I wanted to isolate the effect of behavior while keeping market structure constant.
From a software design perspective, this became an important validation of the architecture.
The simulator was beginning to separate into two layers:
Market Mechanics
and
Agent Psychology.
That separation would become increasingly important as new trader types were introduced.
class MomentumTrader(Agent):
def __init__(self, identity, cash, holdings, risk_tolerance, PnL_memory=None, trend_strength = 1.7, tendency_value=0):
super().__init__(identity, cash, holdings, risk_tolerance, PnL_memory, tendency_value)
self.lookback = random.choice([3,5,7,10])
self.type = "momentum"
self.trend_strength = trend_strength
def compute_tendency(self, price_movement):
noise = random.uniform(-0.2,0.2)
pnl_adjusted = 0
l = min(self.lookback, len(self.pNl))
# to create some randomness amonng Momentumtraders too
for i in range(1,l+1):
pnl_adjusted += self.pNl[-i]
pnl_adjusted = math.tanh(pnl_adjusted) # ----between 0 and 1
new_tendency = self.trend_strength * price_movement + pnl_adjusted + noise
adjusted_tendency = math.tanh(new_tendency) * self.risk_tolerance
self.tendency_val = adjusted_tendency
The Market Started Looking Different
The first thing I noticed was that the market trajectory changed.
Not dramatically.
But consistently.
When a larger fraction of agents followed momentum-based decision rules, price paths began diverging from the baseline simulation.
Nothing about the market mechanism had changed.
Only the agents.
This was my first direct observation of emergent behavior.
The market-level outcome was changing despite the market itself remaining identical.
At this point I felt I had finally crossed an important threshold.
The simulator no longer felt like a collection of formulas.
It felt like a system.
Then Everything Started Looking Wrong
The excitement lasted about fifteen minutes.
Then I started inspecting the internals.
The more I looked at the simulation, the more uncomfortable I became.
The price paths looked reasonable.
But the underlying mechanics didn't.
Buy pressure consistently appeared stronger than sell pressure.
The market drifted upward more often than intuition suggested.
And some simulations felt suspiciously optimistic.
The interesting part was that none of these issues were obvious from looking at the final price chart.
The simulator was producing plausible outcomes while hiding structural implementation problems underneath.
Bug #1 — Accidental Bull Market
One of the first bugs I discovered was buried inside the pricing function.
For small return values I had written:
returns = returns * returns
At first glance this seemed harmless.
I was experimenting with nonlinear market response and wanted larger imbalances to have disproportionate effects.
What I failed to notice was that squaring returns destroys directional information.
Negative returns become positive.
A bearish market signal suddenly becomes bullish.
This meant that under certain conditions:
sell pressure could still contribute to upward price movement.
The market was not merely reacting to imbalance.
It was systematically biased upward.
The bug was subtle because prices still looked believable.
The simulation didn't crash.
The market didn't explode.
It simply developed an unintended bullish drift.
Fixing the problem required preserving sign information:
returns = math.copysign(returns**2, returns)
A single line of code completely changed the behavior of the market.
That experience reinforced something that would become a recurring theme throughout the project:
Small implementation details can create large macroeconomic consequences.
Bug #2 — Buy Pressure Was Stronger Than Sell Pressure
The second issue emerged while inspecting order flow.
Buy volume and sell volume were not actually being generated symmetrically.
Buying power was determined using:
cash / current_price
while selling power was determined using:
holdings × trade_fraction
Although both formulations appeared reasonable in isolation, they operated on fundamentally different scales.
The result was a structural bias toward stronger buying pressure.
The market was not drifting because agents were optimistic.
It was drifting because the implementation gave buyers more influence than sellers.
Again, the lesson wasn't about finance.
It was about simulation design.
Systems can develop persistent behavior from tiny asymmetries that are almost invisible when reading code locally.
The market was revealing assumptions I didn't realize I had encoded.
Momentum Traders Actually Changed The Market
After fixing the major implementation issues, I returned to the original question.
Did behavioral heterogeneity actually matter?
The answer appeared to be yes.
Momentum-heavy populations produced:
larger imbalance spikes
more clustered directional pressure
different return structures
different long-term price evolution
The important observation was not that momentum traders made prices rise.
The important observation was that local decision rules propagated into system-level dynamics.
No agent had access to the entire market.
No agent coordinated with any other agent.
Yet collectively they generated visibly different market behavior.
This was the first moment where the simulator genuinely felt emergent.
The behavior was no longer being programmed directly.
It was arising from interaction.
By the end of this phase I had learned far more from debugging than from implementation.
The Momentum Trader itself was only a few lines of code.
Most of the work involved understanding why the market behaved the way it did.
Several assumptions that initially felt harmless turned out to have enormous influence over aggregate outcomes.
The simulator was slowly becoming less about writing code and more about investigating systems.
The next phase pushed that idea even further.
Instead of adding new behavior and assuming it worked, I started treating the simulator like an experimental laboratory.
That decision led to the most surprising result of the project so far:
Increasing risk tolerance reduced market volatility.
Which was the exact opposite of what I expected.
Then Came The Moment I Realized My Agents Had No Memory
Up to this point, the simulator looked alive.
Prices moved.
Returns fluctuated.
Momentum traders generated different trajectories than baseline agents.
The market appeared dynamic.
But there was a problem.
Every tick, agents effectively forgot who they were.
They reacted to the latest market movement, generated an action, and then disappeared into the next timestep.
There was no meaningful continuity.
No evolving success.
No evolving failure.
No accumulation of experience.
And the more I thought about it, the stranger that felt.
Real market participants do not trade in isolation.
Every decision changes their future.
A profitable trade alters capital.
A loss changes confidence.
A sequence of successful decisions reinforces behavior.
A sequence of failures often destroys it.
My agents had actions.
But they did not yet have history.
That realization forced a redesign of how state flowed through the simulation.
Wealth Became More Important Than Price
The first major architectural change was introducing wealth tracking.
Until now, the simulator primarily observed market-level outcomes.
Price.
Returns.
Imbalance.
Volume.
But none of those metrics tell us whether agents themselves are succeeding.
An upward market does not necessarily imply profitable participants.
A downward market does not necessarily imply failure.
What matters is how individual balance sheets evolve through time.
For that reason I introduced explicit wealth accounting:
wealth = cash + holdings × price
This sounds trivial.
It wasn't.
The moment wealth became a first-class object, the simulator stopped being purely about market behavior and started becoming about adaptation.
Agents could now accumulate gains.
Absorb losses.
And develop histories that persisted across the simulation.
A Weird Discovery: Fear Was Stronger Than Greed
Another question emerged while studying imbalance dynamics.
Should buying and selling affect markets equally?
My initial implementation assumed yes.
An imbalance of +0.4 and an imbalance of -0.4 were treated symmetrically.
But the more I observed the simulation, the less convincing that assumption became.
Real markets often react differently to fear than optimism.
Panic tends to spread faster.
Liquidations tend to be more urgent.
Downward moves often cascade more aggressively than upward moves.
Rather than immediately searching for empirical justification, I decided to treat this as an experiment.
I introduced a simple parameter:
fear_multiplier
which amplified negative imbalance before pricing occurred.
The objective was not realism.
The objective was exploration.
Could a tiny asymmetry in information propagation generate noticeably different market dynamics?
The answer appeared to be yes.
Even small modifications to imbalance processing produced visible changes in return structure and price evolution.
if imbalance < 0:
imbalance *= fear_multiplier
The Architecture Started Maturing
ommit 4 contains a small change that reveals a much larger shift in thinking. What had previously been a simple tendency() function became compute_tendency(). At first glance, it looks like nothing more than a naming improvement. In reality, it marks the beginning of a more deliberate architecture.
The system was no longer treating every action as a single tangled process. Instead, distinct responsibilities started emerging: decision generation, decision execution, and portfolio accounting became separate concerns. This is the kind of evolution that happens when a project begins moving from a prototype toward a real system. The code was no longer just producing outcomes—it was becoming structured enough to explain how those outcomes were produced.
Another important addition was the tracking of buyers, sellers, and holders. This wasn't merely a feature added for plotting or visualization. It was an investment in observability. At some point, a realization had occurred: if behavior cannot be measured, it cannot be studied. Recording how agents behaved made it possible to analyze the market as an experiment rather than simply watch it run. That is the mindset of a researcher, not just a programmer.
Contrarians Arrive
The next milestone was the introduction of the ContrarianTrader. Importantly, the contrarian agent is not the centerpiece of this phase. The real significance lies in what its existence implies.
By this point, the architecture had matured enough to support competing theories of market behavior. Adding a new trading philosophy no longer required redesigning the entire system. It simply meant introducing a different decision rule:
new_tendency = -1 * self.reversion_strength * price_movement
That single line encodes an entirely different worldview. Momentum traders ask a simple question: Is the trend real, and should I follow it? Contrarian traders ask the opposite question: Has the trend gone too far, and should I bet against it?
The beauty is not in either strategy being correct. The beauty is that the framework had become flexible enough to represent both perspectives and allow them to compete under the same market conditions.
The Most Important Plot of This Phase
Among all the visualizations produced during this stage, the most important is the Price Evolution plot, the one where momentum-driven behavior causes prices to diverge.
Its value is not the specific result. The result itself is almost secondary. What matters is what the plot demonstrates.
The market rules remained unchanged. The mechanics of trading, price formation, and execution were held constant. The only thing that changed was agent psychology, the decision-making process inside the traders themselves.
Yet the market outcome changed dramatically.
That is emergence in its purest form. Large-scale behavior appeared not because the environment was altered, but because the beliefs and reactions of individual participants were different. The plot provides a concrete demonstration that collective market dynamics can arise from simple behavioral rules. It is the first clear sign that the simulation is no longer just executing code, it is generating phenomena worth studying.
The biggest lesson from this phase wasn't that momentum traders behave differently from contrarians.
It was that once agents possess memory, wealth, and behavioral identity, the simulator begins to accumulate history.
Markets stop looking like random number generators.
They start looking like ecosystems.
The next phase would focus on a question that emerged naturally from these experiments:
If behavior matters, how much does risk matter?
And surprisingly, the first answer I found was exactly the opposite of what I expected.
